From aa386d80a6f58f9b70b0612284f32ac435643413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Ma=C5=82ysa?= Date: Wed, 23 Oct 2024 23:24:12 +0200 Subject: [PATCH 1/4] sim: some small fixes --- subprojects/sim/src/sim/db/schema.cc | 4 ++-- subprojects/sim/src/web_server/problems/api.cc | 16 ++++++++-------- subprojects/sim/src/web_server/users/api.cc | 10 +++++----- .../sim/src/web_server/web_worker/web_worker.hh | 3 +-- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/subprojects/sim/src/sim/db/schema.cc b/subprojects/sim/src/sim/db/schema.cc index 70b53c01..7c740277 100644 --- a/subprojects/sim/src/sim/db/schema.cc +++ b/subprojects/sim/src/sim/db/schema.cc @@ -280,8 +280,8 @@ const DbSchema& get_schema() { " `full_status` tinyint(3) unsigned NOT NULL," " `score` bigint(20) DEFAULT NULL," " `last_judgment_began_at` datetime DEFAULT NULL," - " `initial_report` mediumblob NOT NULL," - " `final_report` mediumblob NOT NULL," + " `initial_report` mediumblob NOT NULL," // TODO: rename to initial_judgement_protocol + " `final_report` mediumblob NOT NULL," // TODO: rename to full_judgement_protocol " PRIMARY KEY (`id`)," // Submissions API: with user_id " KEY `user_id` (`user_id`,`id`)," diff --git a/subprojects/sim/src/web_server/problems/api.cc b/subprojects/sim/src/web_server/problems/api.cc index 71046e80..68b29a8b 100644 --- a/subprojects/sim/src/web_server/problems/api.cc +++ b/subprojects/sim/src/web_server/problems/api.cc @@ -49,14 +49,14 @@ namespace capabilities = web_server::capabilities; namespace { struct ProblemInfo { - decltype(Problem::id) id{}; - decltype(Problem::visibility) visibility{}; + decltype(Problem::id) id; + decltype(Problem::visibility) visibility; decltype(Problem::name) name; decltype(Problem::label) label; optional owner_id; - optional owner_username; - optional owner_first_name; - optional owner_last_name; + optional owner_username; + optional owner_first_name; + optional owner_last_name; decltype(Problem::created_at) created_at; decltype(Problem::updated_at) updated_at; optional final_submission_full_status; @@ -69,8 +69,8 @@ struct ProblemInfo { ~ProblemInfo() = default; void append_to( - const capabilities::ProblemCapabilities& caps, json_str::ObjectBuilder& obj, + const capabilities::ProblemCapabilities& caps, const std::vector& public_tags, const std::vector& hidden_tags ) { @@ -216,8 +216,8 @@ Response do_list(Context& ctx, uint32_t limit, Condition&& where_cond fill_tags_for_problem(p.id); arr.val_obj([&](auto& obj) { p.append_to( - capabilities::problem(ctx.session, p.visibility, p.owner_id), obj, + capabilities::problem(ctx.session, p.visibility, p.owner_id), public_tags, hidden_tags ); @@ -607,7 +607,7 @@ Response view_problem(Context& ctx, decltype(Problem::id) problem_id) { } json_str::Object obj; - p.append_to(caps, obj, public_tags, hidden_tags); + p.append_to(obj, caps, public_tags, hidden_tags); if (caps.view_simfile) { obj.prop("simfile", simfile); ConfigFile cf; diff --git a/subprojects/sim/src/web_server/users/api.cc b/subprojects/sim/src/web_server/users/api.cc index 87dca88e..30f1861e 100644 --- a/subprojects/sim/src/web_server/users/api.cc +++ b/subprojects/sim/src/web_server/users/api.cc @@ -39,8 +39,8 @@ namespace capabilities = web_server::capabilities; namespace { struct UserInfo { - decltype(User::id) id{}; - decltype(User::type) type{}; + decltype(User::id) id; + decltype(User::type) type; decltype(User::username) username; decltype(User::first_name) first_name; decltype(User::last_name) last_name; @@ -54,7 +54,7 @@ struct UserInfo { UserInfo& operator=(UserInfo&&) = delete; ~UserInfo() = default; - void append_to(const capabilities::UserCapabilities& caps, json_str::ObjectBuilder& obj) { + void append_to(json_str::ObjectBuilder& obj, const capabilities::UserCapabilities& caps) { throw_assert(caps.view); obj.prop("id", id); obj.prop("type", type); @@ -102,7 +102,7 @@ Response do_list(Context& ctx, uint32_t limit, Condition&& where_cond while (stmt.next()) { ++rows_num; arr.val_obj([&](auto& obj) { - u.append_to(capabilities::user(ctx.session, u.id, u.type), obj); + u.append_to(obj, capabilities::user(ctx.session, u.id, u.type)); }); } }); @@ -253,7 +253,7 @@ Response view_user(Context& ctx, decltype(User::id) user_id) { } json_str::Object obj; - u.append_to(capabilities::user(ctx.session, u.id, u.type), obj); + u.append_to(obj, capabilities::user(ctx.session, u.id, u.type)); return ctx.response_json(std::move(obj).into_str()); } diff --git a/subprojects/sim/src/web_server/web_worker/web_worker.hh b/subprojects/sim/src/web_server/web_worker/web_worker.hh index 4e122591..1ee1a58f 100644 --- a/subprojects/sim/src/web_server/web_worker/web_worker.hh +++ b/subprojects/sim/src/web_server/web_worker/web_worker.hh @@ -1,6 +1,5 @@ #pragma once -#include "../http/cookies.hh" #include "../http/request.hh" #include "../http/response.hh" #include "context.hh" @@ -13,7 +12,7 @@ namespace web_server::web_worker { class WebWorker { using UrlDispatcher = ::http::UrlDispatcher; - sim::mysql::Connection& mysql; + sim::mysql::Connection& mysql; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) std::optional request; UrlDispatcher get_dispatcher; UrlDispatcher post_dispatcher; From 30c16948d2aaedf43f06f0a56a5e1044e5c8b2ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Ma=C5=82ysa?= Date: Sun, 22 Sep 2024 16:36:06 +0200 Subject: [PATCH 2/4] sim: refactor: add new submissions API --- subprojects/sim/meson.build | 1 + subprojects/sim/src/sim/db/schema.cc | 94 +- subprojects/sim/src/sim_upgrader.cc | 117 +- .../web_server/capabilities/submissions.cc | 247 +- .../web_server/capabilities/submissions.hh | 130 +- .../sim/src/web_server/submissions/api.cc | 2212 +++++++++++++++++ .../sim/src/web_server/submissions/api.hh | 426 ++++ subprojects/sim/src/web_server/ui_template.cc | 2 +- .../src/web_server/web_worker/web_worker.cc | 81 + 9 files changed, 3256 insertions(+), 54 deletions(-) create mode 100644 subprojects/sim/src/web_server/submissions/api.cc create mode 100644 subprojects/sim/src/web_server/submissions/api.hh diff --git a/subprojects/sim/meson.build b/subprojects/sim/meson.build index bdeb6dc4..a6c06f0e 100644 --- a/subprojects/sim/meson.build +++ b/subprojects/sim/meson.build @@ -249,6 +249,7 @@ sim_server = executable('sim-server', 'src/web_server/problems/ui.cc', 'src/web_server/server/connection.cc', 'src/web_server/server/server.cc', + 'src/web_server/submissions/api.cc', 'src/web_server/ui/ui.cc', 'src/web_server/ui_template.cc', 'src/web_server/users/api.cc', diff --git a/subprojects/sim/src/sim/db/schema.cc b/subprojects/sim/src/sim/db/schema.cc index 7c740277..b5fcd461 100644 --- a/subprojects/sim/src/sim/db/schema.cc +++ b/subprojects/sim/src/sim/db/schema.cc @@ -282,34 +282,73 @@ const DbSchema& get_schema() { " `last_judgment_began_at` datetime DEFAULT NULL," " `initial_report` mediumblob NOT NULL," // TODO: rename to initial_judgement_protocol " `final_report` mediumblob NOT NULL," // TODO: rename to full_judgement_protocol + // Submissions API: all " PRIMARY KEY (`id`)," + // Submissions API: with type + " KEY `type` (`type`,`id`)," + " KEY `problem_final` (`problem_final`,`id`)," + " KEY `contest_problem_final` (`contest_problem_final`,`id`)," // Submissions API: with user_id " KEY `user_id` (`user_id`,`id`)," - " KEY `user_id_2` (`user_id`,`type`,`id`)," - " KEY `user_id_3` (`user_id`,`problem_id`,`id`)," - " KEY `user_id_4` (`user_id`,`contest_problem_id`,`id`)," - " KEY `user_id_5` (`user_id`,`contest_round_id`,`id`)," - " KEY `user_id_6` (`user_id`,`contest_id`,`id`)," - " KEY `user_id_7` (`user_id`,`contest_problem_final`,`id`)," - " KEY `user_id_8` (`user_id`,`problem_final`,`id`)," - " KEY `user_id_9` (`user_id`,`problem_id`,`type`,`id`)," - " KEY `user_id_10` (`user_id`,`problem_id`,`problem_final`)," - " KEY `user_id_11` (`user_id`,`contest_problem_id`,`type`,`id`)," - " KEY `user_id_12` (`user_id`,`contest_problem_id`,`contest_problem_final`)," - " KEY `user_id_13` (`user_id`,`contest_round_id`,`type`,`id`)," - " KEY `user_id_14` (`user_id`,`contest_round_id`,`contest_problem_final`,`id`)," - " KEY `user_id_15` (`user_id`,`contest_id`,`type`,`id`)," - " KEY `user_id_16` (`user_id`,`contest_id`,`contest_problem_final`,`id`)," - // Submissions API: without user_id - " KEY `type` (`type`,`id`)," + // Submissions API: with user_id and type + " KEY `user_id_type` (`user_id`,`type`,`id`)," + " KEY `user_id_problem_final` (`user_id`,`problem_final`,`id`)," + " KEY `user_id_contest_problem_final` (`user_id`,`contest_problem_final`,`id`)," + // Submissions API: with problem_id " KEY `problem_id` (`problem_id`,`id`)," - " KEY `contest_problem_id` (`contest_problem_id`,`id`)," - " KEY `contest_round_id` (`contest_round_id`,`id`)," + // Submissions API: with problem_id and type + " KEY `problem_id_type` (`problem_id`,`type`,`id`)," + " KEY `problem_id_problem_final` (`problem_id`,`problem_final`,`id`)," + " KEY `problem_id_contest_problem_final` (`problem_id`,`contest_problem_final`,`id`)," + // Submissions API: with problem_id and user_id + " KEY `problem_id_user_id` (`problem_id`,`user_id`,`id`)," + // Submissions API: with problem_id and user_id and type + " KEY `problem_id_user_id_type` (`problem_id`,`user_id`,`type`,`id`)," + " KEY `problem_id_user_id_problem_final` (`problem_id`,`user_id`,`problem_final`,`id`)," + " KEY `problem_id_user_id_contest_problem_final` (`problem_id`,`user_id`,`contest_problem_final`,`id`)," + // Submissions API: with contest_id " KEY `contest_id` (`contest_id`,`id`)," - " KEY `problem_id_2` (`problem_id`,`type`,`id`)," - " KEY `contest_problem_id_2` (`contest_problem_id`,`type`,`id`)," - " KEY `contest_round_id_2` (`contest_round_id`,`type`,`id`)," - " KEY `contest_id_2` (`contest_id`,`type`,`id`)," + // Submissions API: with contest_id and type + " KEY `contest_id_type` (`contest_id`,`type`,`id`)," + // Submissions API: with contest_id and type + Contest ranking API + " KEY `contest_id_contest_problem_final` (`contest_id`,`contest_problem_final`,`id`)," + // Contest ranking API: + " KEY `contest_id_contest_problem_initial_final` (`contest_id`,`contest_problem_initial_final`,`id`)," + // Submissions API: with contest_id and user_id + " KEY `contest_id_user_id` (`contest_id`,`user_id`,`id`)," + // Submissions API: with contest_id and user_id and type + " KEY `contest_id_user_id_type` (`contest_id`,`user_id`,`type`,`id`)," + " KEY `contest_id_user_id_contest_problem_final` (`contest_id`,`user_id`,`contest_problem_final`,`id`)," + " KEY `contest_id_user_id_contest_problem_initial_final` (`contest_id`,`user_id`,`contest_problem_initial_final`,`id`)," + // Submissions API: with contest_round_id + " KEY `contest_round_id` (`contest_round_id`,`id`)," + // Submissions API: with contest_round_id and type + " KEY `contest_round_id_type` (`contest_round_id`,`type`,`id`)," + // Submissions API: with contest_round_id and type + Contest round ranking API + " KEY `contest_round_id_contest_problem_final` (`contest_round_id`,`contest_problem_final`,`id`)," + // Contest round ranking API: + " KEY `contest_round_id_contest_problem_initial_final` (`contest_round_id`,`contest_problem_initial_final`,`id`)," + // Submissions API: with contest_round_id and user_id + " KEY `contest_round_id_user_id` (`contest_round_id`,`user_id`,`id`)," + // Submissions API: with contest_round_id and user_id and type + " KEY `contest_round_id_user_id_type` (`contest_round_id`,`user_id`,`type`,`id`)," + " KEY `contest_round_id_user_id_contest_problem_final` (`contest_round_id`,`user_id`,`contest_problem_final`,`id`)," + " KEY `contest_round_id_user_id_contest_problem_initial_final` (`contest_round_id`,`user_id`,`contest_problem_initial_final`,`id`)," + // Submissions API: with contest_problem_id + " KEY `contest_problem_id` (`contest_problem_id`,`id`)," + // Submissions API: with contest_problem_id and type + " KEY `contest_problem_id_type` (`contest_problem_id`,`type`,`id`)," + // Submissions API: with contest_problem_id and type + Contest problem ranking API + " KEY `contest_problem_id_contest_problem_final` (`contest_problem_id`,`contest_problem_final`,`id`)," + // Contest problem ranking API: + " KEY `contest_problem_id_contest_problem_initial_final` (`contest_problem_id`,`contest_problem_initial_final`,`id`)," + // Submissions API: with contest_problem_id and user_id + " KEY `contest_problem_id_user_id` (`contest_problem_id`,`user_id`,`id`)," + // Submissions API: with contest_problem_id and user_id and type + " KEY `contest_problem_id_user_id_type` (`contest_problem_id`,`user_id`,`type`,`id`)," + // Submissions API: with contest_problem_id and user_id and type + contest / contest round / contest problem API: view coloring + " KEY `contest_problem_id_user_id_contest_problem_final` (`contest_problem_id`,`user_id`,`contest_problem_final`)," + " KEY `contest_problem_id_user_id_contest_problem_initial_final` (`contest_problem_id`,`user_id`,`contest_problem_initial_final`)," // Needed to efficiently select problem final submission " KEY `for_problem_final` (`final_candidate`,`user_id`,`problem_id`,`score` DESC,`full_status`,`id` DESC)," // Needed to efficiently select contest problem final submission @@ -320,15 +359,6 @@ const DbSchema& get_schema() { " KEY `for_contest_initial_problem_final_by_initial_status` (`initial_final_candidate`,`user_id`,`contest_problem_id`,`initial_status`,`id` DESC)," " KEY `for_contest_initial_problem_final_by_score_and_initial_status` (`initial_final_candidate`,`user_id`,`contest_problem_id`,`score` DESC,`initial_status`,`id` DESC)," " KEY `for_contest_initial_problem_final_by_score_and_full_status` (`initial_final_candidate`,`user_id`,`contest_problem_id`,`score` DESC,`full_status`,`id` DESC)," - // Needed to efficiently query contest view coloring - " KEY `initial_final` (`user_id`,`contest_problem_id`,`contest_problem_initial_final`)," - // For ranking - " KEY `contest_final` (`contest_id`,`contest_problem_final`)," - " KEY `contest_initial_final` (`contest_id`,`contest_problem_initial_final`)," - " KEY `contest_round_final` (`contest_round_id`,`contest_problem_final`)," - " KEY `contest_initial_round_final` (`contest_round_id`,`contest_problem_initial_final`)," - " KEY `contest_problem_final` (`contest_problem_id`,`contest_problem_final`)," - " KEY `contest_initial_problem_final` (`contest_problem_id`,`contest_problem_initial_final`)," // For foreign keys " KEY `file_id` (`file_id`)," " CONSTRAINT `submissions_ibfk_1` FOREIGN KEY (`file_id`) REFERENCES `internal_files` (`id`) ON UPDATE CASCADE," diff --git a/subprojects/sim/src/sim_upgrader.cc b/subprojects/sim/src/sim_upgrader.cc index 882d4b22..3fa22ffc 100644 --- a/subprojects/sim/src/sim_upgrader.cc +++ b/subprojects/sim/src/sim_upgrader.cc @@ -40,7 +40,7 @@ run_command(const vector& args, const Spawner::Options& options = {}) { // Update the below hash and body of the function do_perform_upgrade() constexpr StringView NORMALIZED_SCHEMA_HASH_BEFORE_UPGRADE = - "935264658c3a283d37e125526d67102dd03f4596fdfb704d970cd227c48d8bda"; + "d9ec1b212056603054feaad1a606b234a361a838aa58cd21ee6b67f1f2460f9f"; static void do_perform_upgrade( [[maybe_unused]] const string& sim_dir, [[maybe_unused]] sim::mysql::Connection& mysql @@ -48,19 +48,112 @@ static void do_perform_upgrade( STACK_UNWINDING_MARK; // Upgrade here + mysql.execute("ALTER TABLE submissions DROP KEY contest_problem_final"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_2"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_3"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_4"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_5"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_6"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_7"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_8"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_9"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_10"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_11"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_12"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_13"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_14"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_15"); + mysql.execute("ALTER TABLE submissions DROP KEY user_id_16"); + mysql.execute("ALTER TABLE submissions DROP KEY problem_id_2"); + mysql.execute("ALTER TABLE submissions DROP KEY contest_id_2"); + mysql.execute("ALTER TABLE submissions DROP KEY contest_problem_id_2"); + mysql.execute("ALTER TABLE submissions DROP KEY contest_round_id_2"); + mysql.execute("ALTER TABLE submissions DROP KEY contest_final"); + mysql.execute("ALTER TABLE submissions DROP KEY contest_initial_final"); + mysql.execute("ALTER TABLE submissions DROP KEY contest_round_final"); + mysql.execute("ALTER TABLE submissions DROP KEY contest_initial_round_final"); + mysql.execute("ALTER TABLE submissions DROP KEY contest_initial_problem_final"); + mysql.execute("ALTER TABLE submissions DROP KEY initial_final"); + mysql.execute("ALTER TABLE submissions ADD KEY `problem_final` (`problem_final`,`id`)"); mysql.execute( - "ALTER TABLE submissions ADD KEY `contest_final` (`contest_id`,`contest_problem_final`)" + "ALTER TABLE submissions ADD KEY `contest_problem_final` (`contest_problem_final`,`id`)" ); - mysql.execute("ALTER TABLE submissions ADD KEY `contest_initial_final` (`contest_id`," - "`contest_problem_initial_final`)"); - mysql.execute("ALTER TABLE submissions ADD KEY `contest_round_final` (`contest_round_id`," - "`contest_problem_final`)"); - mysql.execute("ALTER TABLE submissions ADD KEY `contest_initial_round_final` " - "(`contest_round_id`,`contest_problem_initial_final`)"); - mysql.execute("ALTER TABLE submissions ADD KEY `contest_problem_final` (`contest_problem_id`," - "`contest_problem_final`)"); - mysql.execute("ALTER TABLE submissions ADD KEY `contest_initial_problem_final` " - "(`contest_problem_id`,`contest_problem_initial_final`)"); + mysql.execute( + "ALTER TABLE submissions ADD KEY `user_id_problem_final` (`user_id`,`problem_final`,`id`)" + ); + mysql.execute("ALTER TABLE submissions ADD KEY `user_id_contest_problem_final` " + "(`user_id`,`contest_problem_final`,`id`)"); + mysql.execute("ALTER TABLE submissions ADD KEY `user_id_type` (`user_id`,`type`,`id`)"); + mysql.execute("ALTER TABLE submissions ADD KEY `problem_id_problem_final` " + "(`problem_id`,`problem_final`,`id`)"); + mysql.execute("ALTER TABLE submissions ADD KEY `problem_id_contest_problem_final` " + "(`problem_id`,`contest_problem_final`,`id`)"); + mysql.execute("ALTER TABLE submissions ADD KEY `problem_id_type` (`problem_id`,`type`,`id`)"); + mysql.execute( + "ALTER TABLE submissions ADD KEY `problem_id_user_id` (`problem_id`,`user_id`,`id`)" + ); + mysql.execute("ALTER TABLE submissions ADD KEY `problem_id_user_id_problem_final` " + "(`problem_id`,`user_id`,`problem_final`,`id`)"); + mysql.execute("ALTER TABLE submissions ADD KEY `problem_id_user_id_contest_problem_final` " + "(`problem_id`,`user_id`,`contest_problem_final`,`id`)"); + mysql.execute("ALTER TABLE submissions ADD KEY `problem_id_user_id_type` " + "(`problem_id`,`user_id`,`type`,`id`)"); + mysql.execute("ALTER TABLE submissions ADD KEY `contest_id_type` (`contest_id`,`type`,`id`)"); + mysql.execute("ALTER TABLE submissions ADD KEY `contest_id_contest_problem_final` " + "(`contest_id`,`contest_problem_final`,`id`)"); + mysql.execute("ALTER TABLE submissions ADD KEY `contest_id_contest_problem_initial_final` " + "(`contest_id`,`contest_problem_initial_final`,`id`)"); + mysql.execute( + "ALTER TABLE submissions ADD KEY `contest_id_user_id` (`contest_id`,`user_id`,`id`)" + ); + mysql.execute("ALTER TABLE submissions ADD KEY `contest_id_user_id_type` " + "(`contest_id`,`user_id`,`type`,`id`)"); + mysql.execute("ALTER TABLE submissions ADD KEY `contest_id_user_id_contest_problem_final` " + "(`contest_id`,`user_id`,`contest_problem_final`,`id`)"); + mysql.execute( + "ALTER TABLE submissions ADD KEY `contest_id_user_id_contest_problem_initial_final` " + "(`contest_id`,`user_id`,`contest_problem_initial_final`,`id`)" + ); + mysql.execute( + "ALTER TABLE submissions ADD KEY `contest_round_id_type` (`contest_round_id`,`type`,`id`)" + ); + mysql.execute("ALTER TABLE submissions ADD KEY `contest_round_id_contest_problem_final` " + "(`contest_round_id`,`contest_problem_final`,`id`)"); + mysql.execute( + "ALTER TABLE submissions ADD KEY `contest_round_id_contest_problem_initial_final` " + "(`contest_round_id`,`contest_problem_initial_final`,`id`)" + ); + mysql.execute("ALTER TABLE submissions ADD KEY `contest_round_id_user_id` " + "(`contest_round_id`,`user_id`,`id`)"); + mysql.execute("ALTER TABLE submissions ADD KEY `contest_round_id_user_id_type` " + "(`contest_round_id`,`user_id`,`type`,`id`)"); + mysql.execute( + "ALTER TABLE submissions ADD KEY `contest_round_id_user_id_contest_problem_final` " + "(`contest_round_id`,`user_id`,`contest_problem_final`,`id`)" + ); + mysql.execute( + "ALTER TABLE submissions ADD KEY `contest_round_id_user_id_contest_problem_initial_final` " + "(`contest_round_id`,`user_id`,`contest_problem_initial_final`,`id`)" + ); + mysql.execute("ALTER TABLE submissions ADD KEY `contest_problem_id_type` " + "(`contest_problem_id`,`type`,`id`)"); + mysql.execute("ALTER TABLE submissions ADD KEY `contest_problem_id_contest_problem_final` " + "(`contest_problem_id`,`contest_problem_final`,`id`)"); + mysql.execute( + "ALTER TABLE submissions ADD KEY `contest_problem_id_contest_problem_initial_final` " + "(`contest_problem_id`,`contest_problem_initial_final`,`id`)" + ); + mysql.execute("ALTER TABLE submissions ADD KEY `contest_problem_id_user_id` " + "(`contest_problem_id`,`user_id`,`id`)"); + mysql.execute("ALTER TABLE submissions ADD KEY `contest_problem_id_user_id_type` " + "(`contest_problem_id`,`user_id`,`type`,`id`)"); + mysql.execute( + "ALTER TABLE submissions ADD KEY `contest_problem_id_user_id_contest_problem_final` " + "(`contest_problem_id`,`user_id`,`contest_problem_final`)" + ); + mysql.execute("ALTER TABLE submissions ADD KEY " + "`contest_problem_id_user_id_contest_problem_initial_final` " + "(`contest_problem_id`,`user_id`,`contest_problem_initial_final`)"); } enum class LockKind { diff --git a/subprojects/sim/src/web_server/capabilities/submissions.cc b/subprojects/sim/src/web_server/capabilities/submissions.cc index e53c7281..62bcf491 100644 --- a/subprojects/sim/src/web_server/capabilities/submissions.cc +++ b/subprojects/sim/src/web_server/capabilities/submissions.cc @@ -1,15 +1,252 @@ +#include "problems.hh" #include "submissions.hh" #include "utils.hh" -#include +#include +#include +#include +#include +#include +#include +#include +#include + +using sim::contest_problems::ContestProblem; +using sim::contest_rounds::ContestRound; +using sim::contest_users::ContestUser; +using sim::problems::Problem; +using sim::submissions::Submission; +using sim::users::User; +using web_server::web_worker::Context; + +namespace { + +bool at_least_moderates_contest(std::optional contest_user_mode) { + if (!contest_user_mode) { + return false; + } + // NOLINTNEXTLINE(bugprone-switch-missing-default-case) + switch (*contest_user_mode) { + case ContestUser::Mode::OWNER: + case ContestUser::Mode::MODERATOR: return true; + case ContestUser::Mode::CONTESTANT: return false; + } + return false; +} + +} // namespace namespace web_server::capabilities { -Submissions submissions_for(const decltype(web_worker::Context::session)& session) noexcept { - return Submissions{ +SubmissionsCapabilities submissions(const decltype(Context::session)& session) noexcept { + return { .web_ui_view = session.has_value(), - .view_my = session.has_value(), - .view_all = is_admin(session), + }; +} + +SubmissionsListCapabilities list_submissions(const decltype(Context::session)& session) noexcept { + return { + .query_all = is_admin(session), + .query_with_type_final = is_admin(session), + .query_with_type_problem_final = is_admin(session), + .query_with_type_contest_problem_final = is_admin(session), + .query_with_type_ignored = is_admin(session), + .query_with_type_problem_solution = is_admin(session), + }; +} + +UserSubmissionsListCapabilities list_user_submissions( + const decltype(Context::session)& session, decltype(User::id) user_id +) noexcept { + return { + .query_all = is_self(session, user_id) || is_admin(session), + .query_with_type_final = is_admin(session), + .query_with_type_problem_final = is_admin(session), + .query_with_type_contest_problem_final = is_admin(session), + .query_with_type_ignored = is_self(session, user_id) || is_admin(session), + }; +} + +SubmissionsListCapabilities list_problem_submissions( + const decltype(Context::session)& session, decltype(Problem::owner_id) problem_owner_id +) noexcept { + bool owns_problem = problem_owner_id && is_self(session, *problem_owner_id); + return { + .query_all = is_admin(session) || owns_problem, + .query_with_type_final = is_admin(session) || owns_problem, + .query_with_type_problem_final = is_admin(session) || owns_problem, + .query_with_type_contest_problem_final = is_admin(session) || owns_problem, + .query_with_type_ignored = is_admin(session) || owns_problem, + .query_with_type_problem_solution = is_admin(session) || owns_problem, + }; +} + +UserSubmissionsListCapabilities list_problem_and_user_submissions( + const decltype(Context::session)& session, + decltype(Problem::visibility) problem_visibility, + decltype(Problem::owner_id) problem_owner_id, + decltype(User::id) user_id +) noexcept { + auto list_problem_submissions_caps = list_problem_submissions(session, problem_owner_id); + auto problem_caps = problem(session, problem_visibility, problem_owner_id); + return { + .query_all = list_problem_submissions_caps.query_all || + (problem_caps.view && is_self(session, user_id)), + .query_with_type_final = list_problem_submissions_caps.query_with_type_final, + .query_with_type_problem_final = + list_problem_submissions_caps.query_with_type_problem_final || + (problem_caps.view && is_self(session, user_id)), + .query_with_type_contest_problem_final = + list_problem_submissions_caps.query_with_type_contest_problem_final, + .query_with_type_ignored = list_problem_submissions_caps.query_with_type_ignored || + (problem_caps.view && is_self(session, user_id)), + }; +} + +ContestSubmissionsListCapabilities list_contest_submissions( + const decltype(Context::session)& session, + std::optional session_user_contest_user_mode +) noexcept { + bool at_least_moderates_contest = ::at_least_moderates_contest(session_user_contest_user_mode); + return { + .query_all = is_admin(session) || at_least_moderates_contest, + .query_with_type_contest_problem_final = is_admin(session) || at_least_moderates_contest, + .query_with_type_ignored = is_admin(session) || at_least_moderates_contest, + }; +} + +ContestAndUserSubmissionsListCapabilities list_contest_and_user_submissions( + const decltype(Context::session)& session, + std::optional session_user_contest_user_mode, + decltype(User::id) user_id +) noexcept { + auto contest_submissions_caps = + list_contest_submissions(session, session_user_contest_user_mode); + return { + .query_all = contest_submissions_caps.query_all || is_self(session, user_id), + .query_with_type_contest_problem_final = + contest_submissions_caps.query_with_type_contest_problem_final || + is_self(session, user_id), + .query_with_type_contest_problem_final_always_show_full_results = + contest_submissions_caps.query_with_type_contest_problem_final, + .query_with_type_ignored = + contest_submissions_caps.query_with_type_ignored || is_self(session, user_id), + }; +} + +ContestRoundAndUserSubmissionsListCapabilities list_contest_round_and_user_submissions( + const decltype(web_worker::Context::session)& session, + std::optional session_user_contest_user_mode, + decltype(sim::users::User::id) user_id, + const decltype(sim::contest_rounds::ContestRound::full_results)& full_results, + const std::string& curr_datetime +) { + auto contest_and_user_submissions_caps = + list_contest_and_user_submissions(session, session_user_contest_user_mode, user_id); + return { + .query_all = contest_and_user_submissions_caps.query_all, + .query_with_type_contest_problem_final = + contest_and_user_submissions_caps.query_with_type_contest_problem_final, + .query_with_type_contest_problem_final_show_full_instead_of_initial_results = + contest_and_user_submissions_caps + .query_with_type_contest_problem_final_always_show_full_results || + curr_datetime >= full_results, + .query_with_type_ignored = contest_and_user_submissions_caps.query_with_type_ignored, + }; +} + +SubmissionCapabilities submission( + const decltype(Context::session)& session, + decltype(Submission::user_id) submission_user_id, + decltype(Submission::type) submission_type, + decltype(Problem::visibility) submission_problem_visibility, + decltype(Problem::owner_id) submission_problem_owner_id, + std::optional session_user_contest_user_mode, + std::optional submission_contest_round_full_results, + std::optional + submission_contest_problem_method_of_choosing_final_submission, + std::optional + submission_contest_problem_score_revealing, + const std::string& curr_datetime +) noexcept { + bool at_least_moderates_contest = ::at_least_moderates_contest(session_user_contest_user_mode); + bool owns_submission = submission_user_id && is_self(session, *submission_user_id); + bool owns_problem = + submission_problem_owner_id && is_self(session, *submission_problem_owner_id); + bool is_contest_submission = submission_contest_round_full_results.has_value(); + bool reveal_score = submission_contest_problem_score_revealing && [&] { + // NOLINTNEXTLINE(bugprone-switch-missing-default-case) + switch (*submission_contest_problem_score_revealing) { + case ContestProblem::ScoreRevealing::NONE: return false; + case ContestProblem::ScoreRevealing::ONLY_SCORE: + case ContestProblem::ScoreRevealing::SCORE_AND_FULL_STATUS: return true; + } + return false; + }(); + bool reveal_full_status = submission_contest_problem_score_revealing && [&] { + // NOLINTNEXTLINE(bugprone-switch-missing-default-case) + switch (*submission_contest_problem_score_revealing) { + case ContestProblem::ScoreRevealing::NONE: + case ContestProblem::ScoreRevealing::ONLY_SCORE: return false; + case ContestProblem::ScoreRevealing::SCORE_AND_FULL_STATUS: return true; + } + return false; + }(); + bool contest_problem_final_visible_before_full_results = + submission_contest_problem_method_of_choosing_final_submission && [&] { + // NOLINTNEXTLINE(bugprone-switch-missing-default-case) + switch (*submission_contest_problem_method_of_choosing_final_submission) { + case ContestProblem::MethodOfChoosingFinalSubmission::LATEST_COMPILING: return true; + case ContestProblem::MethodOfChoosingFinalSubmission::HIGHEST_SCORE: + return reveal_score && reveal_full_status; + } + return false; + }(); + bool may_change_type_or_delete = [&] { + switch (submission_type) { + case Submission::Type::NORMAL: + case Submission::Type::IGNORED: return true; + case Submission::Type::PROBLEM_SOLUTION: return false; + } + return false; + }(); + + return { + .view = is_admin(session) || owns_problem || at_least_moderates_contest || owns_submission, + .view_initial_status = + is_admin(session) || owns_problem || at_least_moderates_contest || owns_submission, + .view_full_status = is_admin(session) || owns_problem || at_least_moderates_contest || + (owns_submission && + (!is_contest_submission || reveal_full_status || + curr_datetime >= *submission_contest_round_full_results)), + .view_initial_judgement_protocol = + is_admin(session) || owns_problem || at_least_moderates_contest || owns_submission, + .view_full_judgement_protocol = is_admin(session) || owns_problem || + at_least_moderates_contest || + (owns_submission && + (!is_contest_submission || curr_datetime >= *submission_contest_round_full_results)), + .view_score = is_admin(session) || owns_problem || at_least_moderates_contest || + (owns_submission && + (!is_contest_submission || reveal_score || + curr_datetime >= *submission_contest_round_full_results)), + .view_if_problem_final = is_admin(session) || owns_problem || + (owns_submission && + problem(session, submission_problem_visibility, submission_problem_owner_id).view), + .view_if_contest_problem_initial_final = is_contest_submission && + (is_admin(session) || owns_problem || at_least_moderates_contest || owns_submission), + .view_if_contest_problem_final = is_contest_submission && + (is_admin(session) || owns_problem || at_least_moderates_contest || + (owns_submission && + (contest_problem_final_visible_before_full_results || + curr_datetime >= *submission_contest_round_full_results))), + .download = + is_admin(session) || owns_problem || at_least_moderates_contest || owns_submission, + .change_type = may_change_type_or_delete && + (is_admin(session) || owns_problem || at_least_moderates_contest), + .rejudge = is_admin(session) || owns_problem || at_least_moderates_contest, + .delete_ = may_change_type_or_delete && + (is_admin(session) || owns_problem || at_least_moderates_contest), + .view_related_jobs = is_admin(session), // TODO: check after implementing jobs }; } diff --git a/subprojects/sim/src/web_server/capabilities/submissions.hh b/subprojects/sim/src/web_server/capabilities/submissions.hh index f8f4fb24..f35c56b1 100644 --- a/subprojects/sim/src/web_server/capabilities/submissions.hh +++ b/subprojects/sim/src/web_server/capabilities/submissions.hh @@ -2,14 +2,136 @@ #include "../web_worker/context.hh" +#include +#include +#include +#include +#include +#include +#include + namespace web_server::capabilities { -struct Submissions { +struct SubmissionsCapabilities { bool web_ui_view : 1; - bool view_my : 1; - bool view_all : 1; }; -Submissions submissions_for(const decltype(web_worker::Context::session)& session) noexcept; +SubmissionsCapabilities submissions(const decltype(web_worker::Context::session)& session) noexcept; + +struct SubmissionsListCapabilities { + bool query_all : 1; + bool query_with_type_final : 1; + bool query_with_type_problem_final : 1; + bool query_with_type_contest_problem_final : 1; + bool query_with_type_ignored : 1; + bool query_with_type_problem_solution : 1; +}; + +SubmissionsListCapabilities list_submissions(const decltype(web_worker::Context::session)& session +) noexcept; + +struct UserSubmissionsListCapabilities { + bool query_all : 1; + bool query_with_type_final : 1; + bool query_with_type_problem_final : 1; + bool query_with_type_contest_problem_final : 1; + bool query_with_type_ignored : 1; +}; + +UserSubmissionsListCapabilities list_user_submissions( + const decltype(web_worker::Context::session)& session, decltype(sim::users::User::id) user_id +) noexcept; + +SubmissionsListCapabilities list_problem_submissions( + const decltype(web_worker::Context::session)& session, + decltype(sim::problems::Problem::owner_id) problem_owner_id +) noexcept; + +UserSubmissionsListCapabilities list_problem_and_user_submissions( + const decltype(web_worker::Context::session)& session, + decltype(sim::problems::Problem::visibility) problem_visibility, + decltype(sim::problems::Problem::owner_id) problem_owner_id, + decltype(sim::users::User::id) user_id +) noexcept; + +struct ContestSubmissionsListCapabilities { + bool query_all : 1; + bool query_with_type_contest_problem_final : 1; + bool query_with_type_ignored : 1; +}; + +ContestSubmissionsListCapabilities list_contest_submissions( + const decltype(web_worker::Context::session)& session, + std::optional session_user_contest_user_mode +) noexcept; + +struct ContestAndUserSubmissionsListCapabilities { + bool query_all : 1; + bool query_with_type_contest_problem_final : 1; + bool query_with_type_contest_problem_final_always_show_full_results : 1; + bool query_with_type_ignored : 1; +}; + +ContestAndUserSubmissionsListCapabilities list_contest_and_user_submissions( + const decltype(web_worker::Context::session)& session, + std::optional session_user_contest_user_mode, + decltype(sim::users::User::id) user_id +) noexcept; + +struct ContestRoundAndUserSubmissionsListCapabilities { + bool query_all : 1; + bool query_with_type_contest_problem_final : 1; + bool query_with_type_contest_problem_final_show_full_instead_of_initial_results : 1; + bool query_with_type_ignored : 1; +}; + +ContestRoundAndUserSubmissionsListCapabilities list_contest_round_and_user_submissions( + const decltype(web_worker::Context::session)& session, + std::optional session_user_contest_user_mode, + decltype(sim::users::User::id) user_id, + const decltype(sim::contest_rounds::ContestRound::full_results)& full_results, + const std::string& curr_datetime +); + +constexpr inline auto list_contest_round_submissions = list_contest_submissions; + +constexpr inline auto list_contest_problem_submissions = list_contest_submissions; + +constexpr inline auto list_contest_problem_and_user_submissions = + list_contest_round_and_user_submissions; + +struct SubmissionCapabilities { + bool view : 1; + bool view_initial_status : 1; + bool view_full_status : 1; + bool view_initial_judgement_protocol : 1; + bool view_full_judgement_protocol : 1; + bool view_score : 1; + bool view_if_problem_final : 1; + bool view_if_contest_problem_initial_final : 1; + bool view_if_contest_problem_final : 1; + bool download : 1; + bool change_type : 1; + bool rejudge : 1; + bool delete_ : 1; + bool view_related_jobs : 1; +}; + +SubmissionCapabilities submission( + const decltype(web_worker::Context::session)& session, + decltype(sim::submissions::Submission::user_id) submission_user_id, + decltype(sim::submissions::Submission::type) submission_type, + decltype(sim::problems::Problem::visibility) submission_problem_visibility, + decltype(sim::problems::Problem::owner_id) submission_problem_owner_id, + std::optional session_user_contest_user_mode, + std::optional + submission_contest_round_full_results, + std::optional< + decltype(sim::contest_problems::ContestProblem::method_of_choosing_final_submission)> + submission_contest_problem_method_of_choosing_final_submission, + std::optional + submission_contest_problem_score_revealing, + const std::string& curr_datetime +) noexcept; } // namespace web_server::capabilities diff --git a/subprojects/sim/src/web_server/submissions/api.cc b/subprojects/sim/src/web_server/submissions/api.cc new file mode 100644 index 00000000..4eb6bb22 --- /dev/null +++ b/subprojects/sim/src/web_server/submissions/api.cc @@ -0,0 +1,2212 @@ +#include "../capabilities/submissions.hh" +#include "../http/response.hh" +#include "../web_worker/context.hh" +#include "api.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using sim::contest_problems::ContestProblem; +using sim::contest_rounds::ContestRound; +using sim::contest_users::ContestUser; +using sim::contests::Contest; +using sim::problems::Problem; +using sim::sql::Condition; +using sim::sql::Select; +using sim::submissions::Submission; +using sim::users::User; +using std::optional; +using web_server::http::Response; +using web_server::web_worker::Context; + +namespace capabilities = web_server::capabilities; + +namespace { + +struct SubmissionInfo { + decltype(Submission::id) id; + decltype(Submission::created_at) created_at; + decltype(Submission::user_id) user_id; + std::optional user_username; + std::optional user_first_name; + std::optional user_last_name; + decltype(Submission::problem_id) problem_id; + decltype(Problem::name) problem_name; + decltype(Submission::contest_problem_id) contest_problem_id; + std::optional contest_problem_name; + decltype(Submission::contest_round_id) contest_round_id; + std::optional contest_round_name; + decltype(Submission::contest_id) contest_id; + std::optional contest_name; + decltype(Submission::type) type; + decltype(Submission::language) language; + decltype(Submission::problem_final) is_problem_final; + decltype(Submission::contest_problem_final) is_contest_problem_final; + decltype(Submission::contest_problem_initial_final) is_contest_problem_initial_final; + decltype(Submission::initial_status) initial_status; + decltype(Submission::full_status) full_status; + decltype(Submission::score) score; + + explicit SubmissionInfo() = default; + SubmissionInfo(const SubmissionInfo&) = delete; + SubmissionInfo(SubmissionInfo&&) = delete; + SubmissionInfo& operator=(const SubmissionInfo&) = delete; + SubmissionInfo& operator=(SubmissionInfo&&) = delete; + ~SubmissionInfo() = default; + + void append_to(json_str::ObjectBuilder& obj, const capabilities::SubmissionCapabilities& caps) { + throw_assert(caps.view); + obj.prop("id", id); + obj.prop("created_at", created_at); + if (user_id) { + obj.prop_obj("user", [&](auto& obj) { + obj.prop("id", user_id); + obj.prop("username", user_username); + obj.prop("first_name", user_first_name); + obj.prop("last_name", user_last_name); + }); + } else { + obj.prop("user", nullptr); + } + obj.prop_obj("problem", [&](auto& obj) { + obj.prop("id", problem_id); + obj.prop("name", problem_name); + }); + if (contest_problem_id) { + obj.prop_obj("contest_problem", [&](auto& obj) { + obj.prop("id", contest_problem_id); + obj.prop("name", contest_problem_name); + }); + } else { + obj.prop("contest_problem", nullptr); + } + if (contest_round_id) { + obj.prop_obj("contest_round", [&](auto& obj) { + obj.prop("id", contest_round_id); + obj.prop("name", contest_round_name); + }); + } else { + obj.prop("contest_round", nullptr); + } + if (contest_id) { + obj.prop_obj("contest", [&](auto& obj) { + obj.prop("id", contest_id); + obj.prop("name", contest_name); + }); + } else { + obj.prop("contest", nullptr); + } + obj.prop("type", type); + + if (caps.view_if_problem_final) { + obj.prop("is_problem_final", is_problem_final); + } else { + obj.prop("is_problem_final", nullptr); + } + + if (caps.view_if_contest_problem_initial_final) { + obj.prop("is_contest_problem_initial_final", is_contest_problem_initial_final); + } else { + obj.prop("is_contest_problem_initial_final", nullptr); + } + + if (caps.view_if_contest_problem_final) { + obj.prop("is_contest_problem_final", is_contest_problem_final); + } else { + obj.prop("is_contest_problem_final", nullptr); + } + + obj.prop("language", language); + + if (caps.view_initial_status) { + obj.prop("initial_status", initial_status); + } else { + obj.prop("initial_status", nullptr); + } + + if (caps.view_full_status) { + obj.prop("full_status", full_status); + } else { + obj.prop("full_status", nullptr); + } + + if (caps.view_score) { + obj.prop("score", score); + } else { + obj.prop("score", nullptr); + } + + obj.prop_obj("capabilities", [&](auto& obj) { + obj.prop("view", caps.view); + obj.prop("view_initial_status", caps.view_initial_status); + obj.prop("view_full_status", caps.view_full_status); + obj.prop("view_initial_judgement_protocol", caps.view_initial_judgement_protocol); + obj.prop("view_full_judgement_protocol", caps.view_full_judgement_protocol); + obj.prop("view_score", caps.view_score); + obj.prop("view_if_problem_final", caps.view_if_problem_final); + obj.prop( + "view_if_contest_problem_initial_final", caps.view_if_contest_problem_initial_final + ); + obj.prop("view_if_contest_problem_final", caps.view_if_contest_problem_final); + obj.prop("download", caps.download); + obj.prop("change_type", caps.change_type); + obj.prop("rejudge", caps.rejudge); + obj.prop("delete", caps.delete_); + obj.prop("view_related_jobs", caps.view_related_jobs); + }); + } +}; + +template +Response do_list(Context& ctx, uint32_t limit, Condition&& where_cond) { + STACK_UNWINDING_MARK; + + SubmissionInfo s; + decltype(Problem::visibility) problem_visibility; + decltype(Problem::owner_id) problem_owner_id; + std::optional + contest_problem_method_of_choosing_final_submission; + std::optional contest_problem_score_revealing; + std::optional contest_round_full_results; + std::optional session_user_contest_user_mode; + auto stmt = ctx.mysql.execute( + Select("s.id, s.created_at, s.user_id, u.username, u.first_name, u.last_name, " + "s.problem_id, p.name, p.visibility, p.owner_id, s.contest_problem_id, cp.name, " + "cp.method_of_choosing_final_submission, cp.score_revealing, s.contest_round_id, " + "cr.name, cr.full_results, c.id, c.name, cu.mode, s.type, s.language, " + "s.problem_final, s.contest_problem_final, s.contest_problem_initial_final, " + "s.initial_status, s.full_status, s.score") + .from("submissions s") + .left_join("users u") + .on("u.id=s.user_id") + .inner_join("problems p") + .on("p.id=s.problem_id") + .left_join("contest_problems cp") + .on("cp.id=s.contest_problem_id") + .left_join("contest_rounds cr") + .on("cr.id=s.contest_round_id") + .left_join("contests c") + .on("c.id=s.contest_id") + .left_join("contest_users cu") + .on("cu.contest_id=s.contest_id AND cu.user_id=?", + ctx.session ? optional{ctx.session->user_id} : std::nullopt) + .where(std::move(where_cond)) + .order_by("s.id DESC") + .limit("?", limit) + ); + stmt.res_bind( + s.id, + s.created_at, + s.user_id, + s.user_username, + s.user_first_name, + s.user_last_name, + s.problem_id, + s.problem_name, + problem_visibility, + problem_owner_id, + s.contest_problem_id, + s.contest_problem_name, + contest_problem_method_of_choosing_final_submission, + contest_problem_score_revealing, + s.contest_round_id, + s.contest_round_name, + contest_round_full_results, + s.contest_id, + s.contest_name, + session_user_contest_user_mode, + s.type, + s.language, + s.is_problem_final, + s.is_contest_problem_final, + s.is_contest_problem_initial_final, + s.initial_status, + s.full_status, + s.score + ); + + const auto curr_datetime = utc_mysql_datetime(); + json_str::Object obj; + size_t rows_num = 0; + obj.prop_arr("list", [&](auto& arr) { + while (stmt.next()) { + ++rows_num; + arr.val_obj([&](auto& obj) { + s.append_to( + obj, + capabilities::submission( + ctx.session, + s.user_id, + s.type, + problem_visibility, + problem_owner_id, + session_user_contest_user_mode, + contest_round_full_results, + contest_problem_method_of_choosing_final_submission, + contest_problem_score_revealing, + curr_datetime + ) + ); + }); + } + }); + obj.prop("may_be_more", rows_num == limit); + return ctx.response_json(std::move(obj).into_str()); +} + +struct ProblemInfoForGettingCapabilities { + decltype(Problem::visibility) problem_visibility; + decltype(Problem::owner_id) problem_owner_id; +}; + +optional +get_problem_info_for_capabilities(Context& ctx, decltype(Problem::id) problem_id) { + ProblemInfoForGettingCapabilities res; + auto stmt = + ctx.mysql.execute(Select("visibility, owner_id").from("problems").where("id=?", problem_id) + ); + stmt.res_bind(res.problem_visibility, res.problem_owner_id); + if (!stmt.next()) { + return std::nullopt; + } + return res; +} + +struct ContestInfoForGettingCapabilities { + optional session_user_contest_user_mode; +}; + +ContestInfoForGettingCapabilities +get_contest_info_for_capabilities(Context& ctx, decltype(Contest::id) contest_id) { + ContestInfoForGettingCapabilities res; + auto stmt = + ctx.mysql.execute(Select("mode") + .from("contest_users") + .where( + "contest_id=? AND user_id=?", + contest_id, + ctx.session ? optional{ctx.session->user_id} : std::nullopt + )); + stmt.res_bind(res.session_user_contest_user_mode); + stmt.next(); + return res; +} + +struct ContestRoundInfoForGettingCapabilities { + decltype(ContestRound::full_results) contest_round_full_results; + optional session_user_contest_user_mode; +}; + +optional +get_contest_round_info_for_capabilities(Context& ctx, decltype(ContestRound::id) contest_round_id) { + ContestRoundInfoForGettingCapabilities res; + auto stmt = + ctx.mysql.execute(Select("cr.full_results, cu.mode") + .from("contest_rounds cr") + .left_join("contest_users cu") + .on("cu.user_id=? AND cu.contest_id=cr.contest_id", + ctx.session ? optional{ctx.session->user_id} : std::nullopt) + .where("cr.id=?", contest_round_id)); + stmt.res_bind(res.contest_round_full_results, res.session_user_contest_user_mode); + if (!stmt.next()) { + return std::nullopt; + } + return res; +} + +struct ContestProblemInfoForGettingCapabilities { + decltype(ContestRound::full_results) contest_round_full_results; + optional session_user_contest_user_mode; +}; + +optional get_contest_problem_info_for_capabilities( + Context& ctx, decltype(ContestProblem::id) contest_problem_id +) { + ContestProblemInfoForGettingCapabilities res; + auto stmt = + ctx.mysql.execute(Select("cr.full_results, cu.mode") + .from("contest_problems cp") + .inner_join("contest_rounds cr") + .on("cr.id=cp.contest_round_id") + .left_join("contest_users cu") + .on("cu.user_id=? AND cu.contest_id=cp.contest_id", + ctx.session ? optional{ctx.session->user_id} : std::nullopt) + .where("cp.id=?", contest_problem_id)); + stmt.res_bind(res.contest_round_full_results, res.session_user_contest_user_mode); + if (!stmt.next()) { + return std::nullopt; + } + return res; +} + +} // namespace + +namespace web_server::submissions::api { + +constexpr inline uint32_t FIRST_QUERY_LIMIT = 64; +constexpr inline uint32_t NEXT_QUERY_LIMIT = 128; + +Response list_submissions(Context& ctx) { + STACK_UNWINDING_MARK; + + auto caps = capabilities::list_submissions(ctx.session); + if (!caps.query_all) { + return ctx.response_403(); + } + return do_list(ctx, FIRST_QUERY_LIMIT, Condition("TRUE")); +} + +Response list_submissions_below_id(Context& ctx, decltype(Submission::id) submission_id) { + STACK_UNWINDING_MARK; + + auto caps = capabilities::list_submissions(ctx.session); + if (!caps.query_all) { + return ctx.response_403(); + } + return do_list(ctx, NEXT_QUERY_LIMIT, Condition("s.idproblem_visibility, + problem_info_for_caps->problem_owner_id, + user_id + ); + if (!caps.query_all) { + return ctx.response_403(); + } + return do_list( + ctx, FIRST_QUERY_LIMIT, Condition("s.problem_id=? AND s.user_id=?", problem_id, user_id) + ); +} + +Response list_problem_and_user_submissions_below_id( + Context& ctx, + decltype(Problem::id) problem_id, + decltype(User::id) user_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto problem_info_for_caps = get_problem_info_for_capabilities(ctx, problem_id); + if (!problem_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_problem_and_user_submissions( + ctx.session, + problem_info_for_caps->problem_visibility, + problem_info_for_caps->problem_owner_id, + user_id + ); + if (!caps.query_all) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition("s.problem_id=? AND s.user_id=? AND s.idproblem_visibility, + problem_info_for_caps->problem_owner_id, + user_id + ); + if (!caps.query_with_type_contest_problem_final) { + return ctx.response_403(); + } + return do_list( + ctx, + FIRST_QUERY_LIMIT, + Condition( + "s.problem_id=? AND s.user_id=? AND s.contest_problem_final=1", problem_id, user_id + ) + ); +} + +Response list_problem_and_user_submissions_with_type_contest_problem_final_below_id( + Context& ctx, + decltype(Problem::id) problem_id, + decltype(User::id) user_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto problem_info_for_caps = get_problem_info_for_capabilities(ctx, problem_id); + if (!problem_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_problem_and_user_submissions( + ctx.session, + problem_info_for_caps->problem_visibility, + problem_info_for_caps->problem_owner_id, + user_id + ); + if (!caps.query_with_type_contest_problem_final) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition( + "s.problem_id=? AND s.user_id=? AND s.contest_problem_final=1 AND s.idproblem_visibility, + problem_info_for_caps->problem_owner_id, + user_id + ); + if (!caps.query_with_type_problem_final) { + return ctx.response_403(); + } + return do_list( + ctx, + FIRST_QUERY_LIMIT, + Condition("s.problem_id=? AND s.user_id=? AND s.problem_final=1", problem_id, user_id) + ); +} + +Response list_problem_and_user_submissions_with_type_problem_final_below_id( + Context& ctx, + decltype(Problem::id) problem_id, + decltype(User::id) user_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto problem_info_for_caps = get_problem_info_for_capabilities(ctx, problem_id); + if (!problem_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_problem_and_user_submissions( + ctx.session, + problem_info_for_caps->problem_visibility, + problem_info_for_caps->problem_owner_id, + user_id + ); + if (!caps.query_with_type_problem_final) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition( + "s.problem_id=? AND s.user_id=? AND s.problem_final=1 AND s.idproblem_visibility, + problem_info_for_caps->problem_owner_id, + user_id + ); + if (!caps.query_with_type_final) { + return ctx.response_403(); + } + return do_list( + ctx, + FIRST_QUERY_LIMIT, + Condition( + "s.problem_id=? AND s.user_id=? AND (s.contest_problem_final=1 OR s.problem_final=1)", + problem_id, + user_id + ) + ); +} + +Response list_problem_and_user_submissions_with_type_final_below_id( + Context& ctx, + decltype(Problem::id) problem_id, + decltype(User::id) user_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto problem_info_for_caps = get_problem_info_for_capabilities(ctx, problem_id); + if (!problem_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_problem_and_user_submissions( + ctx.session, + problem_info_for_caps->problem_visibility, + problem_info_for_caps->problem_owner_id, + user_id + ); + if (!caps.query_with_type_final) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition( + "s.problem_id=? AND s.user_id=? AND (s.contest_problem_final=1 OR s.problem_final=1)" + " AND s.idproblem_visibility, + problem_info_for_caps->problem_owner_id, + user_id + ); + if (!caps.query_with_type_ignored) { + return ctx.response_403(); + } + return do_list( + ctx, + FIRST_QUERY_LIMIT, + Condition( + "s.problem_id=? AND s.user_id=? AND s.type=?", + problem_id, + user_id, + Submission::Type::IGNORED + ) + ); +} + +Response list_problem_and_user_submissions_with_type_ignored_below_id( + Context& ctx, + decltype(Problem::id) problem_id, + decltype(User::id) user_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto problem_info_for_caps = get_problem_info_for_capabilities(ctx, problem_id); + if (!problem_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_problem_and_user_submissions( + ctx.session, + problem_info_for_caps->problem_visibility, + problem_info_for_caps->problem_owner_id, + user_id + ); + if (!caps.query_with_type_ignored) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition( + "s.problem_id=? AND s.user_id=? AND s.type=? AND s.id=cr.full_results, s.contest_problem_final=1, s.contest_problem_initial_final=1)", + contest_id, + user_id, + utc_mysql_datetime() + ) + ); +} + +Response list_contest_and_user_submissions_with_type_contest_problem_final_below_id( + Context& ctx, + decltype(Contest::id) contest_id, + decltype(User::id) user_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto contest_info_for_caps = get_contest_info_for_capabilities(ctx, contest_id); + auto caps = capabilities::list_contest_and_user_submissions( + ctx.session, contest_info_for_caps.session_user_contest_user_mode, user_id + ); + if (!caps.query_with_type_contest_problem_final) { + return ctx.response_403(); + } + if (caps.query_with_type_contest_problem_final_always_show_full_results) { + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition( + "s.contest_id=? AND s.user_id=? AND s.contest_problem_final=1 AND s.id=cr.full_results, s.contest_problem_final=1, s.contest_problem_initial_final=1)" + " AND s.idsession_user_contest_user_mode + ); + if (!caps.query_all) { + return ctx.response_403(); + } + return do_list(ctx, FIRST_QUERY_LIMIT, Condition("s.contest_round_id=?", contest_round_id)); +} + +Response list_contest_round_submissions_below_id( + Context& ctx, + decltype(ContestRound::id) contest_round_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto contest_round_info_for_caps = + get_contest_round_info_for_capabilities(ctx, contest_round_id); + if (!contest_round_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_contest_round_submissions( + ctx.session, contest_round_info_for_caps->session_user_contest_user_mode + ); + if (!caps.query_all) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition("s.contest_round_id=? AND s.idsession_user_contest_user_mode + ); + if (!caps.query_with_type_contest_problem_final) { + return ctx.response_403(); + } + return do_list( + ctx, + FIRST_QUERY_LIMIT, + Condition("s.contest_round_id=? AND s.contest_problem_final=1", contest_round_id) + ); +} + +Response list_contest_round_submissions_with_type_contest_problem_final_below_id( + Context& ctx, + decltype(ContestRound::id) contest_round_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto contest_round_info_for_caps = + get_contest_round_info_for_capabilities(ctx, contest_round_id); + if (!contest_round_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_contest_round_submissions( + ctx.session, contest_round_info_for_caps->session_user_contest_user_mode + ); + if (!caps.query_with_type_contest_problem_final) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition( + "s.contest_round_id=? AND s.contest_problem_final=1 AND s.idsession_user_contest_user_mode + ); + if (!caps.query_with_type_ignored) { + return ctx.response_403(); + } + return do_list( + ctx, + FIRST_QUERY_LIMIT, + Condition("s.contest_round_id=? AND s.type=?", contest_round_id, Submission::Type::IGNORED) + ); +} + +Response list_contest_round_submissions_with_type_ignored_below_id( + Context& ctx, + decltype(ContestRound::id) contest_round_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto contest_round_info_for_caps = + get_contest_round_info_for_capabilities(ctx, contest_round_id); + if (!contest_round_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_contest_round_submissions( + ctx.session, contest_round_info_for_caps->session_user_contest_user_mode + ); + if (!caps.query_with_type_ignored) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition( + "s.contest_round_id=? AND s.type=? AND s.idsession_user_contest_user_mode, + user_id, + contest_round_info_for_caps->contest_round_full_results, + utc_mysql_datetime() + ); + if (!caps.query_all) { + return ctx.response_403(); + } + return do_list( + ctx, + FIRST_QUERY_LIMIT, + Condition("s.contest_round_id=? AND s.user_id=?", contest_round_id, user_id) + ); +} + +Response list_contest_round_and_user_submissions_below_id( + Context& ctx, + decltype(ContestRound::id) contest_round_id, + decltype(User::id) user_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto contest_round_info_for_caps = + get_contest_round_info_for_capabilities(ctx, contest_round_id); + if (!contest_round_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_contest_round_and_user_submissions( + ctx.session, + contest_round_info_for_caps->session_user_contest_user_mode, + user_id, + contest_round_info_for_caps->contest_round_full_results, + utc_mysql_datetime() + ); + if (!caps.query_all) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition( + "s.contest_round_id=? AND s.user_id=? AND s.idsession_user_contest_user_mode, + user_id, + contest_round_info_for_caps->contest_round_full_results, + utc_mysql_datetime() + ); + if (!caps.query_with_type_contest_problem_final) { + return ctx.response_403(); + } + return do_list( + ctx, + FIRST_QUERY_LIMIT, + Condition( + concat_tostr( + "s.contest_round_id=? AND s.user_id=? AND s.", + caps.query_with_type_contest_problem_final_show_full_instead_of_initial_results + ? "contest_problem_final" + : "contest_problem_initial_final", + "=1" + ), + contest_round_id, + user_id + ) + ); +} + +Response list_contest_round_and_user_submissions_with_type_contest_problem_final_below_id( + Context& ctx, + decltype(ContestRound::id) contest_round_id, + decltype(User::id) user_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto contest_round_info_for_caps = + get_contest_round_info_for_capabilities(ctx, contest_round_id); + if (!contest_round_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_contest_round_and_user_submissions( + ctx.session, + contest_round_info_for_caps->session_user_contest_user_mode, + user_id, + contest_round_info_for_caps->contest_round_full_results, + utc_mysql_datetime() + ); + if (!caps.query_with_type_contest_problem_final) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition( + concat_tostr( + "s.contest_round_id=? AND s.user_id=? AND s.", + caps.query_with_type_contest_problem_final_show_full_instead_of_initial_results + ? "contest_problem_final" + : "contest_problem_initial_final", + "=1 AND s.idsession_user_contest_user_mode, + user_id, + contest_round_info_for_caps->contest_round_full_results, + utc_mysql_datetime() + ); + if (!caps.query_with_type_ignored) { + return ctx.response_403(); + } + return do_list( + ctx, + FIRST_QUERY_LIMIT, + Condition( + "s.contest_round_id=? AND s.user_id=? AND s.type=?", + contest_round_id, + user_id, + Submission::Type::IGNORED + ) + ); +} + +Response list_contest_round_and_user_submissions_with_type_ignored_below_id( + Context& ctx, + decltype(ContestRound::id) contest_round_id, + decltype(User::id) user_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto contest_round_info_for_caps = + get_contest_round_info_for_capabilities(ctx, contest_round_id); + if (!contest_round_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_contest_round_and_user_submissions( + ctx.session, + contest_round_info_for_caps->session_user_contest_user_mode, + user_id, + contest_round_info_for_caps->contest_round_full_results, + utc_mysql_datetime() + ); + if (!caps.query_with_type_ignored) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition( + "s.contest_round_id=? AND s.user_id=? AND s.type=? AND s.idsession_user_contest_user_mode + ); + if (!caps.query_all) { + return ctx.response_403(); + } + return do_list(ctx, FIRST_QUERY_LIMIT, Condition("s.contest_problem_id=?", contest_problem_id)); +} + +Response list_contest_problem_submissions_below_id( + Context& ctx, + decltype(ContestProblem::id) contest_problem_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto contest_problem_info_for_caps = + get_contest_problem_info_for_capabilities(ctx, contest_problem_id); + if (!contest_problem_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_contest_problem_submissions( + ctx.session, contest_problem_info_for_caps->session_user_contest_user_mode + ); + if (!caps.query_all) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition("s.contest_problem_id=? AND s.idsession_user_contest_user_mode + ); + if (!caps.query_with_type_contest_problem_final) { + return ctx.response_403(); + } + return do_list( + ctx, + FIRST_QUERY_LIMIT, + Condition("s.contest_problem_id=? AND s.contest_problem_final=1", contest_problem_id) + ); +} + +Response list_contest_problem_submissions_with_type_contest_problem_final_below_id( + Context& ctx, + decltype(ContestProblem::id) contest_problem_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto contest_problem_info_for_caps = + get_contest_problem_info_for_capabilities(ctx, contest_problem_id); + if (!contest_problem_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_contest_problem_submissions( + ctx.session, contest_problem_info_for_caps->session_user_contest_user_mode + ); + if (!caps.query_with_type_contest_problem_final) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition( + "s.contest_problem_id=? AND s.contest_problem_final=1 AND s.idsession_user_contest_user_mode + ); + if (!caps.query_with_type_ignored) { + return ctx.response_403(); + } + return do_list( + ctx, + FIRST_QUERY_LIMIT, + Condition( + "s.contest_problem_id=? AND s.type=?", contest_problem_id, Submission::Type::IGNORED + ) + ); +} + +Response list_contest_problem_submissions_with_type_ignored_below_id( + Context& ctx, + decltype(ContestProblem::id) contest_problem_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto contest_problem_info_for_caps = + get_contest_problem_info_for_capabilities(ctx, contest_problem_id); + if (!contest_problem_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_contest_problem_submissions( + ctx.session, contest_problem_info_for_caps->session_user_contest_user_mode + ); + if (!caps.query_with_type_ignored) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition( + "s.contest_problem_id=? AND s.type=? AND s.idsession_user_contest_user_mode, + user_id, + contest_problem_info_for_caps->contest_round_full_results, + utc_mysql_datetime() + ); + if (!caps.query_all) { + return ctx.response_403(); + } + return do_list( + ctx, + FIRST_QUERY_LIMIT, + Condition("s.contest_problem_id=? AND s.user_id=?", contest_problem_id, user_id) + ); +} + +Response list_contest_problem_and_user_submissions_below_id( + Context& ctx, + decltype(ContestProblem::id) contest_problem_id, + decltype(User::id) user_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto contest_problem_info_for_caps = + get_contest_problem_info_for_capabilities(ctx, contest_problem_id); + if (!contest_problem_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_contest_problem_and_user_submissions( + ctx.session, + contest_problem_info_for_caps->session_user_contest_user_mode, + user_id, + contest_problem_info_for_caps->contest_round_full_results, + utc_mysql_datetime() + ); + if (!caps.query_all) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition( + "s.contest_problem_id=? AND s.user_id=? AND s.idsession_user_contest_user_mode, + user_id, + contest_problem_info_for_caps->contest_round_full_results, + utc_mysql_datetime() + ); + if (!caps.query_with_type_contest_problem_final) { + return ctx.response_403(); + } + return do_list( + ctx, + FIRST_QUERY_LIMIT, + Condition( + concat_tostr( + "s.contest_problem_id=? AND s.user_id=? AND s.", + caps.query_with_type_contest_problem_final_show_full_instead_of_initial_results + ? "contest_problem_final" + : "contest_problem_initial_final", + "=1" + ), + contest_problem_id, + user_id + ) + ); +} + +Response list_contest_problem_and_user_submissions_with_type_ignored( + Context& ctx, decltype(ContestProblem::id) contest_problem_id, decltype(User::id) user_id +) { + STACK_UNWINDING_MARK; + + auto contest_problem_info_for_caps = + get_contest_problem_info_for_capabilities(ctx, contest_problem_id); + if (!contest_problem_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_contest_problem_and_user_submissions( + ctx.session, + contest_problem_info_for_caps->session_user_contest_user_mode, + user_id, + contest_problem_info_for_caps->contest_round_full_results, + utc_mysql_datetime() + ); + if (!caps.query_with_type_ignored) { + return ctx.response_403(); + } + return do_list( + ctx, + FIRST_QUERY_LIMIT, + Condition( + "s.contest_problem_id=? AND s.user_id=? AND s.type=?", + contest_problem_id, + user_id, + Submission::Type::IGNORED + ) + ); +} + +Response list_contest_problem_and_user_submissions_with_type_ignored_below_id( + Context& ctx, + decltype(ContestProblem::id) contest_problem_id, + decltype(User::id) user_id, + decltype(Submission::id) submission_id +) { + STACK_UNWINDING_MARK; + + auto contest_problem_info_for_caps = + get_contest_problem_info_for_capabilities(ctx, contest_problem_id); + if (!contest_problem_info_for_caps) { + return ctx.response_404(); + } + auto caps = capabilities::list_contest_problem_and_user_submissions( + ctx.session, + contest_problem_info_for_caps->session_user_contest_user_mode, + user_id, + contest_problem_info_for_caps->contest_round_full_results, + utc_mysql_datetime() + ); + if (!caps.query_with_type_ignored) { + return ctx.response_403(); + } + return do_list( + ctx, + NEXT_QUERY_LIMIT, + Condition( + "s.contest_problem_id=? AND s.user_id=? AND s.type=? AND s.id + contest_problem_method_of_choosing_final_submission; + std::optional contest_problem_score_revealing; + std::optional contest_round_full_results; + std::optional session_user_contest_user_mode; + { + auto stmt = + ctx.mysql.execute(Select("s.user_id, s.type, p.visibility, p.owner_id, " + "cp.method_of_choosing_final_submission, " + "cp.score_revealing, cr.full_results, cu.mode") + .from("submissions s") + .inner_join("problems p") + .on("p.id=s.problem_id") + .left_join("contest_problems cp") + .on("cp.id=s.contest_problem_id") + .left_join("contest_rounds cr") + .on("cr.id=s.contest_round_id") + .left_join("contest_users cu") + .on("cu.contest_id=s.contest_id AND cu.user_id=?", + ctx.session ? optional{ctx.session->user_id} : std::nullopt) + .where("s.id=?", submission_id)); + stmt.res_bind( + s.user_id, + s.type, + problem_visibility, + problem_owner_id, + contest_problem_method_of_choosing_final_submission, + contest_problem_score_revealing, + contest_round_full_results, + session_user_contest_user_mode + ); + if (!stmt.next()) { + return ctx.response_404(); + } + } + auto caps = capabilities::submission( + ctx.session, + s.user_id, + s.type, + problem_visibility, + problem_owner_id, + session_user_contest_user_mode, + contest_round_full_results, + contest_problem_method_of_choosing_final_submission, + contest_problem_score_revealing, + utc_mysql_datetime() + ); + if (!caps.view) { + return ctx.response_403(); + } + + auto stmt = ctx.mysql.execute( + Select("s.created_at, u.username, u.first_name, u.last_name, s.problem_id, p.name, " + "s.contest_problem_id, cp.name, s.contest_round_id, cr.name, s.contest_id, c.name, " + "s.language, s.problem_final, s.contest_problem_final, " + "s.contest_problem_initial_final, s.initial_status, s.full_status, s.score, " + "s.initial_report, s.final_report") + .from("submissions s") + .left_join("users u") + .on("u.id=s.user_id") + .inner_join("problems p") + .on("p.id=s.problem_id") + .left_join("contest_problems cp") + .on("cp.id=s.contest_problem_id") + .left_join("contest_rounds cr") + .on("cr.id=s.contest_round_id") + .left_join("contests c") + .on("c.id=s.contest_id") + .where("s.id=?", submission_id) + ); + decltype(Submission::initial_report) initial_judgement_protocol; + decltype(Submission::final_report) full_judgement_protocol; + stmt.res_bind( + s.created_at, + s.user_username, + s.user_first_name, + s.user_last_name, + s.problem_id, + s.problem_name, + s.contest_problem_id, + s.contest_problem_name, + s.contest_round_id, + s.contest_round_name, + s.contest_id, + s.contest_name, + s.language, + s.is_problem_final, + s.is_contest_problem_final, + s.is_contest_problem_initial_final, + s.initial_status, + s.full_status, + s.score, + initial_judgement_protocol, + full_judgement_protocol + ); + throw_assert(stmt.next()); + + json_str::Object obj; + s.append_to(obj, caps); + if (caps.view_initial_judgement_protocol) { + obj.prop("initial_judgement_protocol", initial_judgement_protocol); + } + if (caps.view_full_judgement_protocol) { + obj.prop("full_judgement_protocol", full_judgement_protocol); + } + return ctx.response_json(std::move(obj).into_str()); +} + +} // namespace web_server::submissions::api diff --git a/subprojects/sim/src/web_server/submissions/api.hh b/subprojects/sim/src/web_server/submissions/api.hh new file mode 100644 index 00000000..469d437f --- /dev/null +++ b/subprojects/sim/src/web_server/submissions/api.hh @@ -0,0 +1,426 @@ +#pragma once + +#include "../http/response.hh" +#include "../web_worker/context.hh" + +#include +#include +#include +#include +#include +#include + +namespace web_server::submissions::api { + +http::Response list_submissions(web_worker::Context& ctx); + +http::Response list_submissions_below_id( + web_worker::Context& ctx, decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_submissions_with_type_contest_problem_final(web_worker::Context& ctx); + +http::Response list_submissions_with_type_contest_problem_final_below_id( + web_worker::Context& ctx, decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_submissions_with_type_problem_final(web_worker::Context& ctx); + +http::Response list_submissions_with_type_problem_final_below_id( + web_worker::Context& ctx, decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_submissions_with_type_final(web_worker::Context& ctx); + +http::Response list_submissions_with_type_final_below_id( + web_worker::Context& ctx, decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_submissions_with_type_ignored(web_worker::Context& ctx); + +http::Response list_submissions_with_type_ignored_below_id( + web_worker::Context& ctx, decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_submissions_with_type_problem_solution(web_worker::Context& ctx); + +http::Response list_submissions_with_type_problem_solution_below_id( + web_worker::Context& ctx, decltype(sim::submissions::Submission::id) submission_id +); + +http::Response +list_user_submissions(web_worker::Context& ctx, decltype(sim::users::User::id) user_id); + +http::Response list_user_submissions_below_id( + web_worker::Context& ctx, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_user_submissions_with_type_contest_problem_final( + web_worker::Context& ctx, decltype(sim::users::User::id) user_id +); + +http::Response list_user_submissions_with_type_contest_problem_final_below_id( + web_worker::Context& ctx, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_user_submissions_with_type_problem_final( + web_worker::Context& ctx, decltype(sim::users::User::id) user_id +); + +http::Response list_user_submissions_with_type_problem_final_below_id( + web_worker::Context& ctx, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_user_submissions_with_type_final( + web_worker::Context& ctx, decltype(sim::users::User::id) user_id +); + +http::Response list_user_submissions_with_type_final_below_id( + web_worker::Context& ctx, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_user_submissions_with_type_ignored( + web_worker::Context& ctx, decltype(sim::users::User::id) user_id +); + +http::Response list_user_submissions_with_type_ignored_below_id( + web_worker::Context& ctx, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response +list_problem_submissions(web_worker::Context& ctx, decltype(sim::problems::Problem::id) problem_id); + +http::Response list_problem_submissions_below_id( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_problem_submissions_with_type_contest_problem_final( + web_worker::Context& ctx, decltype(sim::problems::Problem::id) problem_id +); + +http::Response list_problem_submissions_with_type_contest_problem_final_below_id( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_problem_submissions_with_type_problem_final( + web_worker::Context& ctx, decltype(sim::problems::Problem::id) problem_id +); + +http::Response list_problem_submissions_with_type_problem_final_below_id( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_problem_submissions_with_type_final( + web_worker::Context& ctx, decltype(sim::problems::Problem::id) problem_id +); + +http::Response list_problem_submissions_with_type_final_below_id( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_problem_submissions_with_type_ignored( + web_worker::Context& ctx, decltype(sim::problems::Problem::id) problem_id +); + +http::Response list_problem_submissions_with_type_ignored_below_id( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_problem_submissions_with_type_problem_solution( + web_worker::Context& ctx, decltype(sim::problems::Problem::id) problem_id +); + +http::Response list_problem_submissions_with_type_problem_solution_below_id( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_problem_and_user_submissions( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::users::User::id) user_id +); + +http::Response list_problem_and_user_submissions_below_id( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_problem_and_user_submissions_with_type_contest_problem_final( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::users::User::id) user_id +); + +http::Response list_problem_and_user_submissions_with_type_contest_problem_final_below_id( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_problem_and_user_submissions_with_type_problem_final( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::users::User::id) user_id +); + +http::Response list_problem_and_user_submissions_with_type_problem_final_below_id( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_problem_and_user_submissions_with_type_final( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::users::User::id) user_id +); + +http::Response list_problem_and_user_submissions_with_type_final_below_id( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_problem_and_user_submissions_with_type_ignored( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::users::User::id) user_id +); + +http::Response list_problem_and_user_submissions_with_type_ignored_below_id( + web_worker::Context& ctx, + decltype(sim::problems::Problem::id) problem_id, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response +list_contest_submissions(web_worker::Context& ctx, decltype(sim::contests::Contest::id) contest_id); + +http::Response list_contest_submissions_below_id( + web_worker::Context& ctx, + decltype(sim::contests::Contest::id) contest_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_submissions_with_type_contest_problem_final( + web_worker::Context& ctx, decltype(sim::contests::Contest::id) contest_id +); + +http::Response list_contest_submissions_with_type_contest_problem_final_below_id( + web_worker::Context& ctx, + decltype(sim::contests::Contest::id) contest_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_submissions_with_type_ignored( + web_worker::Context& ctx, decltype(sim::contests::Contest::id) contest_id +); + +http::Response list_contest_submissions_with_type_ignored_below_id( + web_worker::Context& ctx, + decltype(sim::contests::Contest::id) contest_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_and_user_submissions( + web_worker::Context& ctx, + decltype(sim::contests::Contest::id) contest_id, + decltype(sim::users::User::id) user_id +); + +http::Response list_contest_and_user_submissions_below_id( + web_worker::Context& ctx, + decltype(sim::contests::Contest::id) contest_id, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_and_user_submissions_with_type_contest_problem_final( + web_worker::Context& ctx, + decltype(sim::contests::Contest::id) contest_id, + decltype(sim::users::User::id) user_id +); + +http::Response list_contest_and_user_submissions_with_type_contest_problem_final_below_id( + web_worker::Context& ctx, + decltype(sim::contests::Contest::id) contest_id, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_and_user_submissions_with_type_ignored( + web_worker::Context& ctx, + decltype(sim::contests::Contest::id) contest_id, + decltype(sim::users::User::id) user_id +); + +http::Response list_contest_and_user_submissions_with_type_ignored_below_id( + web_worker::Context& ctx, + decltype(sim::contests::Contest::id) contest_id, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_round_submissions( + web_worker::Context& ctx, decltype(sim::contest_rounds::ContestRound::id) contest_round_id +); + +http::Response list_contest_round_submissions_below_id( + web_worker::Context& ctx, + decltype(sim::contest_rounds::ContestRound::id) contest_round_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_round_submissions_with_type_contest_problem_final( + web_worker::Context& ctx, decltype(sim::contest_rounds::ContestRound::id) contest_round_id +); + +http::Response list_contest_round_submissions_with_type_contest_problem_final_below_id( + web_worker::Context& ctx, + decltype(sim::contest_rounds::ContestRound::id) contest_round_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_round_submissions_with_type_ignored( + web_worker::Context& ctx, decltype(sim::contest_rounds::ContestRound::id) contest_round_id +); + +http::Response list_contest_round_submissions_with_type_ignored_below_id( + web_worker::Context& ctx, + decltype(sim::contest_rounds::ContestRound::id) contest_round_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_round_and_user_submissions( + web_worker::Context& ctx, + decltype(sim::contest_rounds::ContestRound::id) contest_round_id, + decltype(sim::users::User::id) user_id +); + +http::Response list_contest_round_and_user_submissions_below_id( + web_worker::Context& ctx, + decltype(sim::contest_rounds::ContestRound::id) contest_round_id, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_round_and_user_submissions_with_type_contest_problem_final( + web_worker::Context& ctx, + decltype(sim::contest_rounds::ContestRound::id) contest_round_id, + decltype(sim::users::User::id) user_id +); + +http::Response list_contest_round_and_user_submissions_with_type_contest_problem_final_below_id( + web_worker::Context& ctx, + decltype(sim::contest_rounds::ContestRound::id) contest_round_id, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_round_and_user_submissions_with_type_ignored( + web_worker::Context& ctx, + decltype(sim::contest_rounds::ContestRound::id) contest_round_id, + decltype(sim::users::User::id) user_id +); + +http::Response list_contest_round_and_user_submissions_with_type_ignored_below_id( + web_worker::Context& ctx, + decltype(sim::contest_rounds::ContestRound::id) contest_round_id, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_problem_submissions( + web_worker::Context& ctx, decltype(sim::contest_problems::ContestProblem::id) contest_problem_id +); + +http::Response list_contest_problem_submissions_below_id( + web_worker::Context& ctx, + decltype(sim::contest_problems::ContestProblem::id) contest_problem_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_problem_submissions_with_type_contest_problem_final( + web_worker::Context& ctx, decltype(sim::contest_problems::ContestProblem::id) contest_problem_id +); + +http::Response list_contest_problem_submissions_with_type_contest_problem_final_below_id( + web_worker::Context& ctx, + decltype(sim::contest_problems::ContestProblem::id) contest_problem_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_problem_submissions_with_type_ignored( + web_worker::Context& ctx, decltype(sim::contest_problems::ContestProblem::id) contest_problem_id +); + +http::Response list_contest_problem_submissions_with_type_ignored_below_id( + web_worker::Context& ctx, + decltype(sim::contest_problems::ContestProblem::id) contest_problem_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_problem_and_user_submissions( + web_worker::Context& ctx, + decltype(sim::contest_problems::ContestProblem::id) contest_problem_id, + decltype(sim::users::User::id) user_id +); + +http::Response list_contest_problem_and_user_submissions_below_id( + web_worker::Context& ctx, + decltype(sim::contest_problems::ContestProblem::id) contest_problem_id, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response list_contest_problem_and_user_submissions_with_type_contest_problem_final( + web_worker::Context& ctx, + decltype(sim::contest_problems::ContestProblem::id) contest_problem_id, + decltype(sim::users::User::id) user_id +); + +http::Response list_contest_problem_and_user_submissions_with_type_ignored( + web_worker::Context& ctx, + decltype(sim::contest_problems::ContestProblem::id) contest_problem_id, + decltype(sim::users::User::id) user_id +); + +http::Response list_contest_problem_and_user_submissions_with_type_ignored_below_id( + web_worker::Context& ctx, + decltype(sim::contest_problems::ContestProblem::id) contest_problem_id, + decltype(sim::users::User::id) user_id, + decltype(sim::submissions::Submission::id) submission_id +); + +http::Response +view_submission(web_worker::Context& ctx, decltype(sim::submissions::Submission::id) submission_id); + +} // namespace web_server::submissions::api diff --git a/subprojects/sim/src/web_server/ui_template.cc b/subprojects/sim/src/web_server/ui_template.cc index 1d850e12..e151f6e8 100644 --- a/subprojects/sim/src/web_server/ui_template.cc +++ b/subprojects/sim/src/web_server/ui_template.cc @@ -144,7 +144,7 @@ std::string sim_template_params(const decltype(web_worker::Context::session)& se } }); obj.prop_obj("submissions", [&](auto& obj) { - obj.prop("ui_view", capabilities::submissions_for(session).web_ui_view); + obj.prop("ui_view", capabilities::submissions(session).web_ui_view); }); obj.prop_obj("users", [&](auto& obj) { const auto caps = capabilities::users(session); diff --git a/subprojects/sim/src/web_server/web_worker/web_worker.cc b/subprojects/sim/src/web_server/web_worker/web_worker.cc index 369c8736..171ffffc 100644 --- a/subprojects/sim/src/web_server/web_worker/web_worker.cc +++ b/subprojects/sim/src/web_server/web_worker/web_worker.cc @@ -4,6 +4,7 @@ #include "../http/response.hh" #include "../problems/api.hh" #include "../problems/ui.hh" +#include "../submissions/api.hh" #include "../ui/ui.hh" #include "../users/api.hh" #include "../users/ui.hh" @@ -44,6 +45,86 @@ WebWorker::WebWorker(sim::mysql::Connection& mysql) : mysql{mysql} { GET("/api/problems/id%3C/{u64}")(problems::api::list_problems_below_id); GET("/api/problems/visibility=/{custom}", decltype(sim::problems::Problem::visibility)::from_str)(problems::api::list_problems_with_visibility); GET("/api/problems/visibility=/{custom}/id%3C/{u64}", decltype(sim::problems::Problem::visibility)::from_str)(problems::api::list_problems_with_visibility_and_below_id); + GET("/api/submission/{u64}")(submissions::api::view_submission); + GET("/api/submissions")(submissions::api::list_submissions); + GET("/api/submissions/contest=/{u64}")(submissions::api::list_contest_submissions); + GET("/api/submissions/contest=/{u64}/id%3C/{u64}")(submissions::api::list_contest_submissions_below_id); + GET("/api/submissions/contest=/{u64}/type=/contest_problem_final")(submissions::api::list_contest_submissions_with_type_contest_problem_final); + GET("/api/submissions/contest=/{u64}/type=/contest_problem_final/id%3C/{u64}")(submissions::api::list_contest_submissions_with_type_contest_problem_final_below_id); + GET("/api/submissions/contest=/{u64}/type=/ignored")(submissions::api::list_contest_submissions_with_type_ignored); + GET("/api/submissions/contest=/{u64}/type=/ignored/id%3C/{u64}")(submissions::api::list_contest_submissions_with_type_ignored_below_id); + GET("/api/submissions/contest=/{u64}/user=/{u64}")(submissions::api::list_contest_and_user_submissions); + GET("/api/submissions/contest=/{u64}/user=/{u64}/id%3C/{u64}")(submissions::api::list_contest_and_user_submissions_below_id); + GET("/api/submissions/contest=/{u64}/user=/{u64}/type=/contest_problem_final")(submissions::api::list_contest_and_user_submissions_with_type_contest_problem_final); + GET("/api/submissions/contest=/{u64}/user=/{u64}/type=/contest_problem_final/id%3C/{u64}")(submissions::api::list_contest_and_user_submissions_with_type_contest_problem_final_below_id); + GET("/api/submissions/contest=/{u64}/user=/{u64}/type=/ignored")(submissions::api::list_contest_and_user_submissions_with_type_ignored); + GET("/api/submissions/contest=/{u64}/user=/{u64}/type=/ignored/id%3C/{u64}")(submissions::api::list_contest_and_user_submissions_with_type_ignored_below_id); + GET("/api/submissions/contest_problem=/{u64}")(submissions::api::list_contest_problem_submissions); + GET("/api/submissions/contest_problem=/{u64}/id%3C/{u64}")(submissions::api::list_contest_problem_submissions_below_id); + GET("/api/submissions/contest_problem=/{u64}/type=/contest_problem_final")(submissions::api::list_contest_problem_submissions_with_type_contest_problem_final); + GET("/api/submissions/contest_problem=/{u64}/type=/contest_problem_final/id%3C/{u64}")(submissions::api::list_contest_problem_submissions_with_type_contest_problem_final_below_id); + GET("/api/submissions/contest_problem=/{u64}/type=/ignored")(submissions::api::list_contest_problem_submissions_with_type_ignored); + GET("/api/submissions/contest_problem=/{u64}/type=/ignored/id%3C/{u64}")(submissions::api::list_contest_problem_submissions_with_type_ignored_below_id); + GET("/api/submissions/contest_problem=/{u64}/user=/{u64}")(submissions::api::list_contest_problem_and_user_submissions); + GET("/api/submissions/contest_problem=/{u64}/user=/{u64}/id%3C/{u64}")(submissions::api::list_contest_problem_and_user_submissions_below_id); + GET("/api/submissions/contest_problem=/{u64}/user=/{u64}/type=/contest_problem_final")(submissions::api::list_contest_problem_and_user_submissions_with_type_contest_problem_final); + GET("/api/submissions/contest_problem=/{u64}/user=/{u64}/type=/ignored")(submissions::api::list_contest_problem_and_user_submissions_with_type_ignored); + GET("/api/submissions/contest_problem=/{u64}/user=/{u64}/type=/ignored/id%3C/{u64}")(submissions::api::list_contest_problem_and_user_submissions_with_type_ignored_below_id); + GET("/api/submissions/contest_round=/{u64}")(submissions::api::list_contest_round_submissions); + GET("/api/submissions/contest_round=/{u64}/id%3C/{u64}")(submissions::api::list_contest_round_submissions_below_id); + GET("/api/submissions/contest_round=/{u64}/type=/contest_problem_final")(submissions::api::list_contest_round_submissions_with_type_contest_problem_final); + GET("/api/submissions/contest_round=/{u64}/type=/contest_problem_final/id%3C/{u64}")(submissions::api::list_contest_round_submissions_with_type_contest_problem_final_below_id); + GET("/api/submissions/contest_round=/{u64}/type=/ignored")(submissions::api::list_contest_round_submissions_with_type_ignored); + GET("/api/submissions/contest_round=/{u64}/type=/ignored/id%3C/{u64}")(submissions::api::list_contest_round_submissions_with_type_ignored_below_id); + GET("/api/submissions/contest_round=/{u64}/user=/{u64}")(submissions::api::list_contest_round_and_user_submissions); + GET("/api/submissions/contest_round=/{u64}/user=/{u64}/id%3C/{u64}")(submissions::api::list_contest_round_and_user_submissions_below_id); + GET("/api/submissions/contest_round=/{u64}/user=/{u64}/type=/contest_problem_final")(submissions::api::list_contest_round_and_user_submissions_with_type_contest_problem_final); + GET("/api/submissions/contest_round=/{u64}/user=/{u64}/type=/contest_problem_final/id%3C/{u64}")(submissions::api::list_contest_round_and_user_submissions_with_type_contest_problem_final_below_id); + GET("/api/submissions/contest_round=/{u64}/user=/{u64}/type=/ignored")(submissions::api::list_contest_round_and_user_submissions_with_type_ignored); + GET("/api/submissions/contest_round=/{u64}/user=/{u64}/type=/ignored/id%3C/{u64}")(submissions::api::list_contest_round_and_user_submissions_with_type_ignored_below_id); + GET("/api/submissions/id%3C/{u64}")(submissions::api::list_submissions_below_id); + GET("/api/submissions/problem=/{u64}")(submissions::api::list_problem_submissions); + GET("/api/submissions/problem=/{u64}/id%3C/{u64}")(submissions::api::list_problem_submissions_below_id); + GET("/api/submissions/problem=/{u64}/type=/contest_problem_final")(submissions::api::list_problem_submissions_with_type_contest_problem_final); + GET("/api/submissions/problem=/{u64}/type=/contest_problem_final/id%3C/{u64}")(submissions::api::list_problem_submissions_with_type_contest_problem_final_below_id); + GET("/api/submissions/problem=/{u64}/type=/final")(submissions::api::list_problem_submissions_with_type_final); + GET("/api/submissions/problem=/{u64}/type=/final/id%3C/{u64}")(submissions::api::list_problem_submissions_with_type_final_below_id); + GET("/api/submissions/problem=/{u64}/type=/ignored")(submissions::api::list_problem_submissions_with_type_ignored); + GET("/api/submissions/problem=/{u64}/type=/ignored/id%3C/{u64}")(submissions::api::list_problem_submissions_with_type_ignored_below_id); + GET("/api/submissions/problem=/{u64}/type=/problem_final")(submissions::api::list_problem_submissions_with_type_problem_final); + GET("/api/submissions/problem=/{u64}/type=/problem_final/id%3C/{u64}")(submissions::api::list_problem_submissions_with_type_problem_final_below_id); + GET("/api/submissions/problem=/{u64}/type=/problem_solution")(submissions::api::list_problem_submissions_with_type_problem_solution); + GET("/api/submissions/problem=/{u64}/type=/problem_solution/id%3C/{u64}")(submissions::api::list_problem_submissions_with_type_problem_solution_below_id); + GET("/api/submissions/problem=/{u64}/user=/{u64}")(submissions::api::list_problem_and_user_submissions); + GET("/api/submissions/problem=/{u64}/user=/{u64}/id%3C/{u64}")(submissions::api::list_problem_and_user_submissions_below_id); + GET("/api/submissions/problem=/{u64}/user=/{u64}/type=/contest_problem_final")(submissions::api::list_problem_and_user_submissions_with_type_contest_problem_final); + GET("/api/submissions/problem=/{u64}/user=/{u64}/type=/contest_problem_final/id%3C/{u64}")(submissions::api::list_problem_and_user_submissions_with_type_contest_problem_final_below_id); + GET("/api/submissions/problem=/{u64}/user=/{u64}/type=/final")(submissions::api::list_problem_and_user_submissions_with_type_final); + GET("/api/submissions/problem=/{u64}/user=/{u64}/type=/final/id%3C/{u64}")(submissions::api::list_problem_and_user_submissions_with_type_final_below_id); + GET("/api/submissions/problem=/{u64}/user=/{u64}/type=/ignored")(submissions::api::list_problem_and_user_submissions_with_type_ignored); + GET("/api/submissions/problem=/{u64}/user=/{u64}/type=/ignored/id%3C/{u64}")(submissions::api::list_problem_and_user_submissions_with_type_ignored_below_id); + GET("/api/submissions/problem=/{u64}/user=/{u64}/type=/problem_final")(submissions::api::list_problem_and_user_submissions_with_type_problem_final); + GET("/api/submissions/problem=/{u64}/user=/{u64}/type=/problem_final/id%3C/{u64}")(submissions::api::list_problem_and_user_submissions_with_type_problem_final_below_id); + GET("/api/submissions/type=/contest_problem_final")(submissions::api::list_submissions_with_type_contest_problem_final); + GET("/api/submissions/type=/contest_problem_final/id%3C/{u64}")(submissions::api::list_submissions_with_type_contest_problem_final_below_id); + GET("/api/submissions/type=/final")(submissions::api::list_submissions_with_type_final); + GET("/api/submissions/type=/final/id%3C/{u64}")(submissions::api::list_submissions_with_type_final_below_id); + GET("/api/submissions/type=/ignored")(submissions::api::list_submissions_with_type_ignored); + GET("/api/submissions/type=/ignored/id%3C/{u64}")(submissions::api::list_submissions_with_type_ignored_below_id); + GET("/api/submissions/type=/problem_final")(submissions::api::list_submissions_with_type_problem_final); + GET("/api/submissions/type=/problem_final/id%3C/{u64}")(submissions::api::list_submissions_with_type_problem_final_below_id); + GET("/api/submissions/type=/problem_solution")(submissions::api::list_submissions_with_type_problem_solution); + GET("/api/submissions/type=/problem_solution/id%3C/{u64}")(submissions::api::list_submissions_with_type_problem_solution_below_id); + GET("/api/submissions/user=/{u64}")(submissions::api::list_user_submissions); + GET("/api/submissions/user=/{u64}/id%3C/{u64}")(submissions::api::list_user_submissions_below_id); + GET("/api/submissions/user=/{u64}/type=/contest_problem_final")(submissions::api::list_user_submissions_with_type_contest_problem_final); + GET("/api/submissions/user=/{u64}/type=/contest_problem_final/id%3C/{u64}")(submissions::api::list_user_submissions_with_type_contest_problem_final_below_id); + GET("/api/submissions/user=/{u64}/type=/final")(submissions::api::list_user_submissions_with_type_final); + GET("/api/submissions/user=/{u64}/type=/final/id%3C/{u64}")(submissions::api::list_user_submissions_with_type_final_below_id); + GET("/api/submissions/user=/{u64}/type=/ignored")(submissions::api::list_user_submissions_with_type_ignored); + GET("/api/submissions/user=/{u64}/type=/ignored/id%3C/{u64}")(submissions::api::list_user_submissions_with_type_ignored_below_id); + GET("/api/submissions/user=/{u64}/type=/problem_final")(submissions::api::list_user_submissions_with_type_problem_final); + GET("/api/submissions/user=/{u64}/type=/problem_final/id%3C/{u64}")(submissions::api::list_user_submissions_with_type_problem_final_below_id); GET("/api/user/{u64}")(users::api::view_user); GET("/api/user/{u64}/problems")(problems::api::list_user_problems); GET("/api/user/{u64}/problems/id%3C/{u64}")(problems::api::list_user_problems_below_id); From fc9a33b3311386342fa508c5bfa91af8a5883d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Ma=C5=82ysa?= Date: Sun, 3 Nov 2024 14:10:19 +0100 Subject: [PATCH 3/4] sim: ui: submissions page: use new API to list submissions --- subprojects/sim/meson.build | 1 + .../sim/src/web_server/old/submissions.cc | 26 - .../sim/src/web_server/static/kit/scripts.js | 578 ++++++++++++------ .../sim/src/web_server/static/kit/styles.css | 88 ++- .../sim/src/web_server/submissions/ui.cc | 14 + .../sim/src/web_server/submissions/ui.hh | 10 + subprojects/sim/src/web_server/ui_template.cc | 27 + .../src/web_server/web_worker/web_worker.cc | 2 + 8 files changed, 504 insertions(+), 242 deletions(-) create mode 100644 subprojects/sim/src/web_server/submissions/ui.cc create mode 100644 subprojects/sim/src/web_server/submissions/ui.hh diff --git a/subprojects/sim/meson.build b/subprojects/sim/meson.build index a6c06f0e..4655f80e 100644 --- a/subprojects/sim/meson.build +++ b/subprojects/sim/meson.build @@ -250,6 +250,7 @@ sim_server = executable('sim-server', 'src/web_server/server/connection.cc', 'src/web_server/server/server.cc', 'src/web_server/submissions/api.cc', + 'src/web_server/submissions/ui.cc', 'src/web_server/ui/ui.cc', 'src/web_server/ui_template.cc', 'src/web_server/users/api.cc', diff --git a/subprojects/sim/src/web_server/old/submissions.cc b/subprojects/sim/src/web_server/old/submissions.cc index dbde4e35..f33994d9 100644 --- a/subprojects/sim/src/web_server/old/submissions.cc +++ b/subprojects/sim/src/web_server/old/submissions.cc @@ -86,32 +86,6 @@ void Sim::submissions_handle() { if (is_digit(next_arg)) { // View submission page_template(from_unsafe{concat("Submission ", next_arg)}); append("view_submission(false, ", next_arg, ", window.location.hash);"); - - } else if (next_arg.empty()) { // List submissions - page_template("Submissions"); - // clang-format off - append("document.body.appendChild(elem_with_text('h1', 'Submissions'));" - "$(document).ready(function(){" - "var main = $('body');" - "var tabs = [];" - "if (signed_user_is_admin()) {" - "tabs.push('All submissions', function() {" - "main.children('.old_tabmenu + div, .loader,.loader-info').remove();" - "tab_submissions_lister($('
').appendTo(main),'', true);" - "});" - "}" - "if (is_signed_in()) {" - "tabs.push('My submissions', function() {" - "main.children('.old_tabmenu + div, .loader,.loader-info').remove();" - "tab_submissions_lister($('
').appendTo(main)," - "'/u' + signed_user_id);" - "});" - "}" - "old_tabmenu(function(x) { x.appendTo(main); }, tabs);" - "});" - ); - // clang-format on - } else { return error404(); } diff --git a/subprojects/sim/src/web_server/static/kit/scripts.js b/subprojects/sim/src/web_server/static/kit/scripts.js index fa46de4b..99e7e027 100644 --- a/subprojects/sim/src/web_server/static/kit/scripts.js +++ b/subprojects/sim/src/web_server/static/kit/scripts.js @@ -123,6 +123,8 @@ function url_api_problems_with_visibility(problem_visibility) { return `/api/pro function url_api_sign_in() { return '/api/sign_in'; } function url_api_sign_out() { return '/api/sign_out'; } function url_api_sign_up() { return '/api/sign_up'; } +function url_api_submissions() { return '/api/submissions'; } +function url_api_submissions_with_type(submission_type) { return `/api/submissions/type=/${submission_type}`; } function url_api_use_contest_entry_token(contest_entry_token) { return `/api/contest_entry_token/${contest_entry_token}/use`; } function url_api_user(user_id) { return `/api/user/${user_id}`; } function url_api_user_change_password(user_id) { return `/api/user/${user_id}/change_password`; } @@ -131,10 +133,15 @@ function url_api_user_edit(user_id) { return `/api/user/${user_id}/edit`; } function url_api_user_merge_into_another(user_id) { return `/api/user/${user_id}/merge_into_another`; } function url_api_user_problems(user_id) { return `/api/user/${user_id}/problems`; } function url_api_user_problems_with_visibility(user_id, problem_visibility) { return `/api/user/${user_id}/problems/visibility=/${problem_visibility}`; } +function url_api_user_submissions(user_id) { return `/api/submissions/user=/${user_id}`; } +function url_api_user_submissions_with_type(user_id, submission_type) { return `/api/submissions/user=/${user_id}/type=/${submission_type}`; } function url_api_users() { return '/api/users'; } function url_api_users_add() { return '/api/users/add'; } function url_api_users_with_type(user_type) { return `/api/users/type=/${user_type}`; } function url_change_user_password(user_id) { return `/user/${user_id}/change_password`; } +function url_contest(contest_id) { return `/c/c${contest_id}`; } +function url_contest_problem(contest_problem_id) { return `/c/p${contest_problem_id}`; } +function url_contest_round(contest_round_id) { return `/c/r${contest_round_id}`; } function url_contests() { return '/c'; } function url_enter_contest(contest_entry_token) { return `/enter_contest/${contest_entry_token}`; } function url_jobs() { return '/jobs'; } @@ -156,7 +163,9 @@ function url_sign_in() { return '/sign_in'; } function url_sign_out() { return '/sign_out'; } function url_sign_up() { return '/sign_up'; } function url_sim_logo_img() { return '/kit/img/sim-logo.png'; } -function url_submissions() { return '/s'; } +function url_submission(submission_id) { return `/s/${submission_id}`; } +function url_submission_source(submission_id) { return `/s/${submission_id}#source`; } +function url_submissions() { return '/submissions'; } function url_user(user_id) { return `/u/${user_id}`; } function url_user_delete(user_id) { return `/user/${user_id}/delete`; } function url_user_edit(user_id) { return `/user/${user_id}/edit`; } @@ -237,38 +246,26 @@ function humanize_file_size(size) { /* ================================= Ajax ================================= */ /** - * Default values in init: { - * body: null, - * timeout: 0, // no timeout - * show_upload_progress: false, - * response_type: 'text', // accepted values: 'text', 'json' - * remove_previous_status: true, - * show_abort_button_after: 1500, // in milliseconds - * do_before_send: null, // function to call just before sending the request; the advantage - * // of this over just calling it before calling do_xhr_with_status() - * // is that it is called after 'Try again' is clicked - * onloadend: null, // function to call after the request completes (either by success or error) - * extra_http_headers: {}, // additional HTTP headers to set before sending request, format: - * // extra_https_headers: { - * // 'some-header': 'some value', - * // 'another-header': 'another value', - * // } - * } - * * @p process_response is called with parameters response and request_status. Before @p process_response is called, * the response-status is hidden, @p process_response can show it by calling `request_status.show_success(text_message)`. */ -function do_xhr_with_status(method, url, init, parent_elem_for_status, process_response) { - init.body ??= null; - init.timeout ??= 0; - init.show_upload_progress ??= false; - init.response_type ??= 'text'; - init.remove_previous_status ??= true; - init.show_abort_button_after ??= 1500; - init.do_before_send ??= null; - init.onloadend ??= null; - init.extra_http_headers ??= []; - +function do_xhr_with_status(method, url, { + body = null, + timeout = 0, // no timeout + show_upload_progress = false, + response_type = 'text', // accepted values: 'text', 'json' + remove_previous_status = true, + show_abort_button_after = 1500, // in milliseconds + do_before_send = null, // function to call just before sending the request; the advantage + // of this over just calling it before calling do_xhr_with_status() + // is that it is called after 'Try again' is clicked + onloadend = null, // function to call after the request completes (either by success or error) + extra_http_headers = {}, // additional HTTP headers to set before sending request, format: + // extra_https_headers: { + // 'some-header': 'some value', + // 'another-header': 'another value', + // } +}, parent_elem_for_status, process_response) { const status_center_elem = elem_of('center', elem_request_status_pending()); const show_error = (msg) => { const new_status = elem_request_status_error(); @@ -277,7 +274,17 @@ function do_xhr_with_status(method, url, init, parent_elem_for_status, process_r const try_again_btn = elem_with_text('a', 'Try again'); try_again_btn.addEventListener('click', () => { status_center_elem.remove(); - do_xhr_with_status(method, url, init, parent_elem_for_status, process_response); + do_xhr_with_status(method, url, { + body, + timeout, + show_upload_progress, + response_type, + remove_previous_status, + show_abort_button_after, + do_before_send, + onloadend, + extra_http_headers, + }, parent_elem_for_status, process_response); }, {passive: true}); new_status.appendChild(elem_of('center', try_again_btn)); @@ -320,7 +327,7 @@ function do_xhr_with_status(method, url, init, parent_elem_for_status, process_r let parsed_response; try { parsed_response = (() => { - switch (init.response_type) { + switch (response_type) { case 'text': return xhr.response; case 'json': return xhr.response.length === 0 ? undefined : JSON.parse(xhr.response); default: @@ -334,7 +341,7 @@ function do_xhr_with_status(method, url, init, parent_elem_for_status, process_r return; } if (invalid_init_response_type) { - throw Error("Invalid init.response_type = " + init.response_type); + throw Error("Invalid response_type = " + response_type); } status_center_elem.style.display = 'none'; @@ -365,13 +372,13 @@ function do_xhr_with_status(method, url, init, parent_elem_for_status, process_r xhr.ontimeout = () => { show_error('Request timeout'); }; - xhr.onloadend = init.onloadend?.bind(null); + xhr.onloadend = onloadend?.bind(null); let bounded_progress_elem; let unbounded_progress_elem; let progress_elem; let progress_info_elem; - if (init.show_upload_progress) { + if (show_upload_progress) { xhr.upload.onprogress = (e) => { // e.total can be smaller that e.loaded e.g. when we select /dev/urandom if (e.lengthComputable && e.loaded <= e.total) { @@ -391,17 +398,17 @@ function do_xhr_with_status(method, url, init, parent_elem_for_status, process_r } xhr.open(method, url); - xhr.timeout = init.timeout; + xhr.timeout = timeout; xhr.responseType = 'text'; - for (const name in init.extra_http_headers) { - xhr.setRequestHeader(name, init.extra_http_headers[name]); + for (const name in extra_http_headers) { + xhr.setRequestHeader(name, extra_http_headers[name]); } // Append request-status element - if (init.remove_previous_status) { + if (remove_previous_status) { try_remove_centered_request_status(parent_elem_for_status); } append_with_fade_in_slide_down(parent_elem_for_status, status_center_elem); - if (init.show_upload_progress) { + if (show_upload_progress) { const request_progress = elem_with_class('div', 'request-progress'); bounded_progress_elem = request_progress.appendChild(document.createElement('progress')); unbounded_progress_elem = request_progress.appendChild(document.createElement('progress')); @@ -415,11 +422,11 @@ function do_xhr_with_status(method, url, init, parent_elem_for_status, process_r xhr.abort(); }, {passive: true}); abort_btn.style.display = 'none'; - setTimeout(() => { abort_btn.style.display = ''; }, init.show_abort_button_after); + setTimeout(() => { abort_btn.style.display = ''; }, show_abort_button_after); status_center_elem.appendChild(elem_of('center', abort_btn)); - init.do_before_send?.call(null); - xhr.send(init.body); + do_before_send?.call(null); + xhr.send(body); } function FormState(parent_state) { @@ -1567,7 +1574,7 @@ async function change_user_password(user_id) { const user = await view.get_from_api(url_api_user(user_id)); const is_me = (user.id === signed_user_id); const title = is_me ? 'Change my password' - : 'Change password of ' + user.first_name + ' ' + user.last_name; + : 'Change password of\n' + user.first_name + ' ' + user.last_name; const form = new Form(title, url_api_user_change_password(user.id)); if (!user.capabilities.change_password_without_old_password) { form.append_input_password('old_password', 'Old password', 24, {required: false}); @@ -1583,7 +1590,7 @@ async function delete_user(user_id) { const view = new View(url_user_delete(user_id)); const user = await view.get_from_api(url_api_user(user_id)); const is_me = (user.id === signed_user_id); - const title = is_me ? 'Delete account' : 'Delete user ' + user.id; + const title = is_me ? 'Delete account' : 'Delete user\n' + user.first_name + ' ' + user.last_name; const form = new Form(title, url_api_user_delete(user.id), { css_classes: 'with-notice', }); @@ -1722,7 +1729,7 @@ function UsersLister(elem, query_url) { row.appendChild(elem_with_class_and_text('td', user.type, snake_case_to_user_string(user.type))); const td = document.createElement('td'); - td.append.apply(td, ActionsToHTML.user(user, false)); + td.append.apply(td, ActionsToHTML.user(user)); row.appendChild(td); document_fragment.appendChild(row); } @@ -1889,16 +1896,13 @@ async function reupload_problem(problem_id) { form.attach_to(view.content_elem); } -function ProblemsLister(elem, query_url, list_capabilties, { - show_owner_column, - show_updated_at_column, +function ProblemsLister(elem, query_url, { + show_owner_column = true, + show_updated_at_column = true, }) { if (this === window) { throw new Error('Call as "new ProblemsLister()", not "ProblemsLister()"'); } - show_owner_column ??= true; - show_updated_at_column ??= true; - const self = this; self.process_first_api_response = (list) => { @@ -1949,12 +1953,12 @@ function ProblemsLister(elem, query_url, list_capabilties, { } if (show_owner_column) { - const owner_td = row.appendChild(document.createElement('td')); + const td = row.appendChild(document.createElement('td')); if (problem.capabilities.view_owner) { if (problem.owner != null) { - owner_td.appendChild(a_view_button(url_user(problem.owner.id), problem.owner.username, undefined, view_user.bind(null, true, problem.owner.id))); // TODO: refactor it + td.appendChild(a_view_button(url_user(problem.owner.id), problem.owner.username, undefined, view_user.bind(null, true, problem.owner.id))); // TODO: refactor it } else if (problem.capabilities.view_owner) { - owner_td.textContent = '(Deleted)'; + td.textContent = '(Deleted)'; } } } @@ -1964,14 +1968,7 @@ function ProblemsLister(elem, query_url, list_capabilties, { } if (problem.capabilities.view_final_submission_full_status && problem.final_submission_full_status != null) { - switch (problem.final_submission_full_status) { - case 'ok': row.classList.add('status', 'green'); break; - case 'wa': row.classList.add('status', 'red'); break; - case 'tle': row.classList.add('status', 'yellow'); break; - case 'mle': row.classList.add('status', 'yellow'); break; - case 'rte': row.classList.add('status', 'intense-red'); break; - default: assert(false, 'unexpected final_submission_full_status'); - } + row.classList.add(...submission_status_to_css_class_list(problem.final_submission_full_status)); } const td = document.createElement('td'); @@ -1992,24 +1989,24 @@ function list_problems() { view.content_elem.appendChild(elem_link_with_class_to_view('btn', 'Add problem', add_problem, url_problems_add)); } - const retab = (url_all_func, url_by_type_func, list_capabilities, extra_params, elem) => { - const retab = (url, list_capabilities, elem) => { + const retab = (url_all_func, url_with_visibility_func, list_capabilities, extra_params, elem) => { + const do_retab = (url, elem) => { const table = elem.appendChild(elem_with_class('table', 'problems stripped')); - new ProblemsLister(table, url, list_capabilities, extra_params); + new ProblemsLister(table, url, extra_params); }; const tabmenu = new TabMenuBuilder(); if (list_capabilities.query_all) { - tabmenu.add_tab('All', retab.bind(null, url_all_func(), list_capabilities)); + tabmenu.add_tab('All', do_retab.bind(null, url_all_func())); } if (list_capabilities.query_with_visibility_public) { - tabmenu.add_tab('Public', retab.bind(null, url_by_type_func('public'), list_capabilities)); + tabmenu.add_tab('Public', do_retab.bind(null, url_with_visibility_func('public'))); } if (list_capabilities.query_with_visibility_contest_only) { - tabmenu.add_tab('Contest only', retab.bind(null, url_by_type_func('contest_only'), list_capabilities)); + tabmenu.add_tab('Contest only', do_retab.bind(null, url_with_visibility_func('contest_only'))); } if (list_capabilities.query_with_visibility_private) { - tabmenu.add_tab('Private', retab.bind(null, url_by_type_func('private'), list_capabilities)); + tabmenu.add_tab('Private', do_retab.bind(null, url_with_visibility_func('private'))); } tabmenu.build_and_append_to(elem); }; @@ -2047,6 +2044,241 @@ function list_problems() { tabmenu.build_and_append_to(view.content_elem); } +function submission_language_to_user_string(submission_language) { + switch (submission_language) { + case 'c11': return 'C11'; + case 'cpp11': return 'C++11'; + case 'pascal': return 'Pascal'; + case 'cpp14': return 'C++14'; + case 'cpp17': return 'C++17'; + case 'python': return 'Python'; + case 'rust': return 'Rust'; + case 'cpp20': return 'C++20'; + default: assert(false, 'unexpected submission_language: ' + submission_language); + } +} + +function submission_status_to_css_class_list(submission_status) { + switch (submission_status) { + case 'ok': return ['status', 'green']; + case 'wa': return ['status', 'red']; + case 'tle': return ['status', 'yellow']; + case 'mle': return ['status', 'yellow']; + case 'ole': return ['status', 'yellow']; + case 'rte': return ['status', 'intense-red']; + case 'pending': return ['status']; + case 'compilation_error': return ['status', 'purple']; + case 'checker_compilation_error': return ['status', 'blue']; + case 'judge_error': return ['status', 'blue']; + default: assert(false, 'unexpected submission_status: ', submission_status); + } +} + +function submission_status_to_user_string(submission_status) { + switch (submission_status) { + case 'ok': return 'OK'; + case 'wa': return 'Wrong answer'; + case 'tle': return 'Time limit exceeded'; + case 'mle': return 'Memory limit exceeded'; + case 'ole': return 'Output size limit exceeded'; + case 'rte': return 'Runtime error'; + case 'pending': return 'Pending'; + case 'compilation_error': return 'Compilation error'; + case 'checker_compilation_error': return 'Checker compilation error'; + case 'judge_error': return 'Judge error'; + default: assert(false, 'unexpected submission_status: ', submission_status); + } +} + +function submission_type_to_user_string(submission, show_if_problem_final) { + switch (submission.type) { + case 'normal': { + let res = ''; + if (show_if_problem_final && submission.capabilities.view_if_problem_final && submission.is_problem_final) { + res = 'Problem'; + } + if (submission.capabilities.view_if_contest_problem_final && submission.is_contest_problem_final) { + if (res == '') { + res = 'Contest'; + } else { + res += ' + contest' + } + } + if (!submission.capabilities.view_if_contest_problem_final && submission.capabilities.view_if_contest_problem_initial_final && submission.is_contest_problem_initial_final) { + if (res == '') { + res = 'Contest initial'; + } else { + res += ' + contest initial'; + } + } + if (res == '') { + return 'Normal'; + } else { + res += ' final'; + return res; + } + } + case 'ignored': return 'Ignored'; + case 'problem_solution': return 'Problem solution'; + } +} + +function SubmissionsLister(elem, query_url, { + show_user_column = true, + show_if_problem_final = true, +}) { + if (this === window) { + throw new Error('Call as "new SubmissionsLister()", not "SubmissionsLister()"'); + } + + const self = this; + self.process_first_api_response = (list) => { + if (list.length === 0) { + self.elem.parentNode.appendChild(elem_with_text('p', 'There are no submissions to show...')); + return; + } + + const thead = document.createElement('thead'); + thead.appendChild(elem_with_text('th', 'Id')); + thead.appendChild(elem_with_class_and_text('th', 'language', 'Lang')); + if (show_user_column) { + thead.appendChild(elem_with_class_and_text('th', 'user', 'User')); + } + thead.appendChild(elem_with_class_of('th', 'created_at', 'Added', elem_timezone_marker())); + thead.appendChild(elem_with_class_and_text('th', 'problem', 'Problem')); + thead.appendChild(elem_with_class_and_text('th', 'status', 'Status')); + thead.appendChild(elem_with_class_and_text('th', 'score', 'Score')); + thead.appendChild(elem_with_class_and_text('th', 'type', 'Type')); + thead.appendChild(elem_with_class_and_text('th', 'actions', 'Actions')); + self.elem.appendChild(thead); + self.tbody = document.createElement('tbody'); + self.elem.appendChild(self.tbody); + + self.process_api_response(list); + }; + self.process_api_response = (list) => { + self.next_query_suffix = '/id { + const table = elem.appendChild(elem_with_class('table', 'submissions')); + new SubmissionsLister(table, url, extra_params); + }; + + const tabmenu = new TabMenuBuilder(); + if (list_capabilities.query_all) { + tabmenu.add_tab('All', retab.bind(null, url_all_func())); + } + if (list_capabilities.query_with_type_final) { + tabmenu.add_tab('Final', retab.bind(null, url_with_type_func('final'))); + } + if (list_capabilities.query_with_type_problem_final) { + tabmenu.add_tab('Problem final', retab.bind(null, url_with_type_func('problem_final'))); + } + if (list_capabilities.query_with_type_contest_problem_final) { + tabmenu.add_tab('Contest final', retab.bind(null, url_with_type_func('contest_problem_final'))); + } + if (list_capabilities.query_with_type_ignored) { + tabmenu.add_tab('Ignored', retab.bind(null, url_with_type_func('ignored'))); + } + if (list_capabilities.query_with_type_problem_solution) { + tabmenu.add_tab('Problem solutions', retab.bind(null, url_with_type_func('problem_solution'))); + } + tabmenu.build_and_append_to(elem); +} + +function can_list_any_submissions(list_capabilities) { + return list_capabilities.query_all || list_capabilities.query_with_type_final || + list_capabilities.query_with_type_problem_final || + list_capabilities.query_with_type_contest_problem_final || + list_capabilities.query_with_type_ignored || + list_capabilities.query_with_type_problem_solution; +} + +function list_submissions() { + const view = new View(url_submissions()); + view.content_elem.appendChild(elem_with_text('h1', 'Submissions')); + + const tabmenu = new TabMenuBuilder(); + if (can_list_any_submissions(global_capabilities.submissions.list_all)) { + tabmenu.add_tab('All submissions', append_tabbed_submissions_lister.bind( + null, + url_api_submissions, + url_api_submissions_with_type, + global_capabilities.submissions.list_all, + {} + )); + } + if (is_signed_in() && can_list_any_submissions(global_capabilities.submissions.list_my)) { + tabmenu.add_tab('My submissions', append_tabbed_submissions_lister.bind( + null, + url_api_user_submissions.bind(null, signed_user_id), + url_api_user_submissions_with_type.bind(null, signed_user_id), + global_capabilities.submissions.list_my, + { + show_user_column: false, + } + )); + } + tabmenu.build_and_append_to(view.content_elem); +} + ////////////////////////// The above code has gone through refactor ////////////////////////// function text_to_safe_html(str) { // This is deprecated because DOM elements have innerText property (see elem_with_text() function) @@ -3354,7 +3586,104 @@ function tab_logs_view(parent_elem) { } /* ============================ Actions buttons ============================ */ -var ActionsToHTML = {}; +const ActionsToHTML = {}; + +ActionsToHTML.user = function(user, viewing_user = false) { + let res = []; + if (!viewing_user && user.capabilities.view) { + res.push(a_view_button(url_user(user.id), 'View', 'btn-small', + view_user.bind(null, true, user.id))); + } + if (user.capabilities.edit) { + res.push(a_view_button(url_user_edit(user.id), 'Edit', + 'btn-small blue', edit_user.bind(null, user.id))); + } + if (user.capabilities.delete) { + res.push(a_view_button(url_user_delete(user.id), 'Delete', + 'btn-small red', delete_user.bind(null, user.id))); + } + if (user.capabilities.merge_into_another_user) { + res.push(a_view_button(url_user_merge_into_another(user.id), + 'Merge', 'btn-small red', + merge_user.bind(null, user.id))); + } + if (user.capabilities.change_password || user.capabilities.change_password_without_old_password) { + res.push(a_view_button(url_change_user_password(user.id), + 'Change password', 'btn-small orange', + change_user_password.bind(null, user.id))); + } + return res; +}; + +ActionsToHTML.problem = function(problem, viewing_problem = false) { + let res = []; + if (!viewing_problem && problem.capabilities.view) { + res.push(a_view_button(url_problem(problem.id), 'View', 'btn-small', + view_problem.bind(null, true, problem.id))); + } + if (problem.capabilities.view_statement) { + const a = elem_with_class_and_text('a', 'btn-small', 'Statement'); + a.href = url_problem_statement(problem.id, problem.name); + res.push(a); + } + if (problem.capabilities.create_submission) { + res.push(a_view_button(url_problem_create_submission(problem.id), 'Submit', + 'btn-small blue', add_problem_submission.bind(null, true, {id: problem.id}))); + } + if (problem.capabilities.view_solutions) { + res.push(a_view_button(url_problem_solutions(problem.id), + 'Solutions', 'btn-small', view_problem.bind(null, true, problem.id, + '#all_submissions#solutions'))); + } + if (viewing_problem && problem.capabilities.download) { + const a = elem_with_class_and_text('a', 'btn-small', 'Download'); + a.href = url_problem_download(problem.id); + res.push(a); + } + if (problem.capabilities.edit) { + res.push(a_view_button(url_problem_edit(problem.id), 'Edit', + 'btn-small blue', edit_problem.bind(null, true, problem.id))); + } + if (viewing_problem && problem.capabilities.delete) { + res.push(a_view_button(url_problem_delete(problem.id), 'Delete', + 'btn-small red', delete_problem.bind(null, true, problem.id))); + } + if (viewing_problem && problem.capabilities.merge) { + res.push(a_view_button(url_problem_merge(problem.id), 'Merge', + 'btn-small red', merge_problem.bind(null, true, problem.id))); + } + if (viewing_problem && problem.capabilities.rejudge_all_submissions) { + const a = elem_with_class_and_text('a', 'btn-small blue', 'Rejudge all submissions'); + a.addEventListener('click', rejudge_problem_submissions.bind(null, problem.id, problem.name), {passive: true}); + res.push(a); + } + if (viewing_problem && problem.capabilities.reset_time_limits) { + res.push(a_view_button(url_problem_reset_time_limits(problem.id), 'Reset time limits', + 'btn-small blue', reset_problem_time_limits.bind(null, true, problem.id))); + } + return res; +}; + +ActionsToHTML.submission = function(submission, viewing_submission = false) { + let res = []; + if (!viewing_submission && submission.capabilities.view) { + res.push(a_view_button(url_submission(submission.id), 'View', 'btn-small', view_submission.bind(null, true, submission.id))); + } + if (!viewing_submission && submission.capabilities.download) { + res.push(a_view_button(url_submission_source(submission.id), 'Source', 'btn-small', view_submission.bind(null, true, submission.id, '#source'))); + } + if (submission.capabilities.change_type) { + res.push(a_view_button(undefined, 'Change type', 'btn-small orange', submission_chtype.bind(null, submission.id, submission.type))); + } + if (submission.capabilities.rejudge) { + res.push(a_view_button(undefined, 'Rejudge', 'btn-small blue', rejudge_submission.bind(null, submission.id))); + } + if (submission.capabilities.delete) { + res.push(a_view_button(undefined, 'Delete', 'btn-small red', delete_submission.bind(null, submission.id))); + } + return res; +}; + ActionsToHTML.job = function(job_id, actions_str, problem_id, job_view /*= false*/) { if (job_view == undefined) job_view = false; @@ -3403,36 +3732,7 @@ ActionsToHTML.job = function(job_id, actions_str, problem_id, job_view /*= false return res; }; -ActionsToHTML.user = function(user, user_view /*= false*/) { - if (user_view === undefined) - user_view = false; - var res = []; - if (!user_view && user.capabilities.view) { - res.push(a_view_button(url_user(user.id), 'View', 'btn-small', - view_user.bind(null, true, user.id))); - } - if (user.capabilities.edit) { - res.push(a_view_button(url_user_edit(user.id), 'Edit', - 'btn-small blue', edit_user.bind(null, user.id))); - } - if (user.capabilities.delete) { - res.push(a_view_button(url_user_delete(user.id), 'Delete', - 'btn-small red', delete_user.bind(null, user.id))); - } - if (user.capabilities.merge_into_another_user) { - res.push(a_view_button(url_user_merge_into_another(user.id), - 'Merge', 'btn-small red', - merge_user.bind(null, user.id))); - } - if (user.capabilities.change_password || user.capabilities.change_password_without_old_password) { - res.push(a_view_button(url_change_user_password(user.id), - 'Change password', 'btn-small orange', - change_user_password.bind(null, user.id))); - } - return res; -}; - -ActionsToHTML.submission = function(submission_id, actions_str, submission_type, submission_view /*= false*/) { +ActionsToHTML.oldsubmission = function(submission_id, actions_str, submission_type, submission_view /*= false*/) { if (submission_view === undefined) submission_view = false; @@ -3470,68 +3770,6 @@ ActionsToHTML.submission = function(submission_id, actions_str, submission_type, return res; }; -ActionsToHTML.problem = function(problem, problem_view /*= false*/) { - if (problem_view === undefined) - problem_view = false; - - var res = []; - if (!problem_view && problem.capabilities.view) { - res.push(a_view_button(url_problem(problem.id), 'View', 'btn-small', - view_problem.bind(null, true, problem.id))); - } - - if (problem.capabilities.view_statement) { - const a = elem_with_class_and_text('a', 'btn-small', 'Statement'); - a.href = url_problem_statement(problem.id, problem.name); - res.push(a); - } - - if (problem.capabilities.create_submission) { - res.push(a_view_button(url_problem_create_submission(problem.id), 'Submit', - 'btn-small blue', add_problem_submission.bind(null, true, {id: problem.id}))); - } - - if (problem.capabilities.view_solutions) { - res.push(a_view_button(url_problem_solutions(problem.id), - 'Solutions', 'btn-small', view_problem.bind(null, true, problem.id, - '#all_submissions#solutions'))); - } - - if (problem_view && problem.capabilities.download) { - const a = elem_with_class_and_text('a', 'btn-small', 'Download'); - a.href = url_problem_download(problem.id); - res.push(a); - } - - if (problem.capabilities.edit) { - res.push(a_view_button(url_problem_edit(problem.id), 'Edit', - 'btn-small blue', edit_problem.bind(null, true, problem.id))); - } - - if (problem_view && problem.capabilities.delete) { - res.push(a_view_button(url_problem_delete(problem.id), 'Delete', - 'btn-small red', delete_problem.bind(null, true, problem.id))); - } - - if (problem_view && problem.capabilities.merge) { - res.push(a_view_button(url_problem_merge(problem.id), 'Merge', - 'btn-small red', merge_problem.bind(null, true, problem.id))); - } - - if (problem_view && problem.capabilities.rejudge_all_submissions) { - const a = elem_with_class_and_text('a', 'btn-small blue', 'Rejudge all submissions'); - a.addEventListener('click', rejudge_problem_submissions.bind(null, problem.id, problem.name), {passive: true}); - res.push(a); - } - - if (problem_view && problem.capabilities.reset_time_limits) { - res.push(a_view_button(url_problem_reset_time_limits(problem.id), 'Reset time limits', - 'btn-small blue', reset_problem_time_limits.bind(null, true, problem.id))); - } - - return res; -}; - ActionsToHTML.oldproblem = function(problem, problem_view /*= false*/) { if (problem_view === undefined) problem_view = false; @@ -4180,12 +4418,12 @@ function submission_chtype(submission_id, submission_type) { $('