feat(contest): contest resources (#40)
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Baoshuo Ren 2023-02-12 21:59:18 +08:00 committed by GitHub
commit 433e56c3eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 381 additions and 151 deletions

View File

@ -181,7 +181,7 @@ CREATE TABLE `contests` (
`last_min` int NOT NULL, `last_min` int NOT NULL,
`player_num` int NOT NULL DEFAULT '0', `player_num` int NOT NULL DEFAULT '0',
`status` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, `status` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`extra_config` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '{}', `extra_config` json NOT NULL,
`zan` int NOT NULL DEFAULT '0', `zan` int NOT NULL DEFAULT '0',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `status` (`status`,`id`) USING BTREE KEY `status` (`status`,`id`) USING BTREE
@ -995,7 +995,8 @@ INSERT INTO `upgrades` (`name`, `status`, `updated_at`) VALUES
('21_problem_difficulty', 'up', now()), ('21_problem_difficulty', 'up', now()),
('28_remote_judge', 'up', now()), ('28_remote_judge', 'up', now()),
('31_problem_resources', 'up', now()), ('31_problem_resources', 'up', now()),
('36_decimal_score_range', 'up', now()); ('36_decimal_score_range', 'up', now()),
('40_contest_resources', 'up', now());
/*!40000 ALTER TABLE `upgrades` ENABLE KEYS */; /*!40000 ALTER TABLE `upgrades` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;

View File

@ -71,8 +71,9 @@ $time_form->handle = function (&$vdata) {
DB::insert([ DB::insert([
"insert into contests", "insert into contests",
"(name, start_time, last_min, status)", "values", DB::bracketed_fields(["name", "start_time", "last_min", "status", "extra_config"]),
DB::tuple([$vdata['name'], $start_time_str, $vdata['last_min'], 'unfinished']) "values",
DB::tuple([$vdata['name'], $start_time_str, $vdata['last_min'], 'unfinished', "{}"])
]); ]);
}; };
$time_form->succ_href = "/contests"; $time_form->succ_href = "/contests";

View File

@ -64,7 +64,7 @@ if (UOJContest::cur()->userCanStartFinalTest(Auth::user())) {
$start_test_form->config['confirm']['smart'] = true; $start_test_form->config['confirm']['smart'] = true;
$start_test_form->runAtServer(); $start_test_form->runAtServer();
} }
if ($contest['cur_progress'] >= CONTEST_TESTING && UOJContest::cur()->queryJudgeProgress()['fully_judged']) { if ($contest['cur_progress'] == CONTEST_TESTING && UOJContest::cur()->queryJudgeProgress()['fully_judged']) {
$publish_result_form = new UOJForm('publish_result'); $publish_result_form = new UOJForm('publish_result');
$publish_result_form->handle = function () { $publish_result_form->handle = function () {
UOJContest::announceOfficialResults(); UOJContest::announceOfficialResults();
@ -492,11 +492,7 @@ function echoSelfReviews() {
<div class="row"> <div class="row">
<?php if ($cur_tab == 'standings' || $cur_tab == 'after_contest_standings' || $cur_tab == 'self_reviews') : ?> <div <?php if ($cur_tab == 'standings' || $cur_tab == 'after_contest_standings' || $cur_tab == 'self_reviews') : ?> class="col-12" <?php else : ?> class="col-md-9" <?php endif ?>>
<div class="col-12">
<?php else : ?>
<div class="col-md-9">
<?php endif ?>
<?= HTML::tablist($tabs_info, $cur_tab, 'nav-pills') ?> <?= HTML::tablist($tabs_info, $cur_tab, 'nav-pills') ?>
<?php if ($cur_tab == 'standings' || $cur_tab == 'after_contest_standings' || $cur_tab == 'self_reviews') : ?> <?php if ($cur_tab == 'standings' || $cur_tab == 'after_contest_standings' || $cur_tab == 'self_reviews') : ?>
<div class="d-none d-md-block text-center"> <div class="d-none d-md-block text-center">
@ -537,15 +533,14 @@ function echoSelfReviews() {
<?php if ($cur_tab == 'standings' || $cur_tab == 'after_contest_standings' || $cur_tab == 'self_reviews') : ?> <?php if ($cur_tab == 'standings' || $cur_tab == 'after_contest_standings' || $cur_tab == 'self_reviews') : ?>
<?php else : ?> <?php else : ?>
<div class="d-md-none"> <hr class="d-md-none" />
<hr />
</div>
<div class="col-md-3"> <div class="col-md-3">
<div class="card card-default mb-2"> <div class="card card-default mb-2">
<div class="card-body"> <div class="card-body">
<h3 class="h4 card-title text-center"> <h3 class="h4 card-title text-center">
<a class="text-decoration-none text-body" href="/contest/<?= $contest['id'] ?>"> <a class="text-decoration-none text-body" href="<?= UOJContest::cur()->getUri() ?>">
<?= $contest['name'] ?> <?= UOJContest::info('name') ?>
</a> </a>
</h3> </h3>
<div class="card-text text-center text-muted"> <div class="card-text text-center text-muted">
@ -555,24 +550,22 @@ function echoSelfReviews() {
$('#contest-countdown').countdown(<?= $contest['end_time']->getTimestamp() - UOJTime::$time_now->getTimestamp() ?>, function() {}, '1.75rem', false); $('#contest-countdown').countdown(<?= $contest['end_time']->getTimestamp() - UOJTime::$time_now->getTimestamp() ?>, function() {}, '1.75rem', false);
</script> </script>
<?php elseif ($contest['cur_progress'] <= CONTEST_TESTING) : ?> <?php elseif ($contest['cur_progress'] <= CONTEST_TESTING) : ?>
<?php if ($contest['cur_progress'] < CONTEST_TESTING) : ?> <?php $judge_progress = UOJContest::cur()->queryJudgeProgress() ?>
<?= UOJLocale::get('contests::contest pending final test') ?> <?= $judge_progress['title'] ?> (<?= $judge_progress['rop'] ?>%)
<?php else : ?>
<?php
$total = DB::selectCount("select count(*) from submissions where contest_id = {$contest['id']}");
$n_judged = DB::selectCount("select count(*) from submissions where contest_id = {$contest['id']} and status = 'Judged'");
$rop = $total == 0 ? 100 : (int)($n_judged / $total * 100);
?>
<?= UOJLocale::get('contests::final testing') ?>
(<?= $rop ?>%)
<?php endif ?>
<?php else : ?> <?php else : ?>
<?= UOJLocale::get('contests::contest ended') ?> <?= UOJLocale::get('contests::contest ended') ?>
<?php endif ?> <?php endif ?>
</div> </div>
</div> </div>
<div class="card-footer bg-transparent"> <div class="list-group list-group-flush">
比赛评价:<?= UOJContest::cur()->getZanBlock() ?> <li class="list-group-item d-flex justify-content-between align-items-center">
<span class="flex-shrink-0">
<?= UOJLocale::get('appraisal') ?>
</span>
<span>
<?= UOJContest::cur()->getZanBlock() ?>
</span>
</li>
</div> </div>
</div> </div>
@ -581,17 +574,17 @@ function echoSelfReviews() {
<p><strong>注意:比赛时只显示测样例的结果。</strong></p> <p><strong>注意:比赛时只显示测样例的结果。</strong></p>
<?php elseif (UOJContest::cur()->basicRule() === 'ACM') : ?> <?php elseif (UOJContest::cur()->basicRule() === 'ACM') : ?>
<p>此次比赛为 ACM 赛制。</p> <p>此次比赛为 ACM 赛制。</p>
<p><strong>封榜时间:<?= $contest['frozen_time']->format('Y-m-d H:i:s') ?></strong></p> <p><strong>封榜时间:<?= $contest['frozen_time']->format(UOJTime::FORMAT) ?></strong></p>
<?php elseif (UOJContest::cur()->basicRule() === 'IOI') : ?> <?php elseif (UOJContest::cur()->basicRule() === 'IOI') : ?>
<p>此次比赛为 IOI 赛制。</p> <p>此次比赛为 IOI 赛制。</p>
<p>比赛时显示的得分即最终得分。</p> <p>比赛时显示的得分即最终得分。</p>
<?php endif ?> <?php endif ?>
<a href="/contest/<?= $contest['id'] ?>/registrants" class="btn btn-secondary d-block mt-2"> <a href="<?= UOJContest::cur()->getUri('/registrants') ?>" class="btn btn-secondary d-block mt-2">
<?= UOJLocale::get('contests::contest registrants') ?> <?= UOJLocale::get('contests::contest registrants') ?>
</a> </a>
<?php if ($is_manager) : ?> <?php if ($is_manager) : ?>
<a href="/contest/<?= $contest['id'] ?>/manage" class="btn btn-primary d-block mt-2"> <a href="<?= UOJContest::cur()->getUri('/manage') ?>" class="btn btn-primary d-block mt-2">
管理 管理
</a> </a>
<?php endif ?> <?php endif ?>
@ -605,20 +598,26 @@ function echoSelfReviews() {
<?php $publish_result_form->printHTML(); ?> <?php $publish_result_form->printHTML(); ?>
</div> </div>
<?php endif ?> <?php endif ?>
<?php if ($contest['extra_config']['links']) : ?>
<div class="card card-default border-info mt-3"> <!-- 附件 -->
<div class="card-header bg-info"> <div class="card mt-3">
<h3 class="card-title">比赛资料</h3> <div class="card-header fw-bold">
<?= UOJLocale::get('contests::links') ?>
</div> </div>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<?php foreach ($contest['extra_config']['links'] as $link) : ?> <a class="list-group-item list-group-item-action" href="<?= HTML::url(UOJContest::cur()->getResourcesBaseUri()) ?>">
<a href="/blogs/<?= $link[1] ?>" class="list-group-item"><?= $link[0] ?></a> <?= UOJLocale::get('contests::resources') ?>
</a>
<?php foreach (UOJContest::cur()->getAdditionalLinks() as $link) : ?>
<a class="list-group-item list-group-item-action" href="<?= $link['url'] ?>">
<?= HTML::escape($link['name']) ?>
</a>
<?php endforeach ?> <?php endforeach ?>
</div> </div>
</div> </div>
<?php endif ?>
</div> </div>
<?php endif ?> <?php endif ?>
</div> </div>
<?php echoUOJPageFooter() ?> <?php echoUOJPageFooter() ?>

View File

@ -359,7 +359,7 @@ EOD);
dieWithJsonData(['status' => 'success', 'message' => '修改成功']); dieWithJsonData(['status' => 'success', 'message' => '修改成功']);
}; };
$rule_form->setAjaxSubmit(<<<EOD $rule_form->setAjaxSubmit(<<<EOD
function(res) { function(res) {
if (res.status === 'success') { if (res.status === 'success') {
$('#type-result-alert') $('#type-result-alert')
.html('修改成功!') .html('修改成功!')
@ -374,10 +374,141 @@ function(res) {
.show(); .show();
} }
setTimeout(function() {
$('#type-result-alert').hide();
}, 5000);
$(window).scrollTop(0); $(window).scrollTop(0);
} }
EOD); EOD);
$rule_form->runAtServer(); $rule_form->runAtServer();
$links = UOJContest::cur()->getAdditionalLinks();
$links_str = json_encode($links, JSON_FORCE_OBJECT);
$links_form = new UOJForm('links');
$links_form->add('contest_links', '', function ($str, &$vdata) {
$data = json_decode($str, true);
$new_data = [];
if ($data === null) return '不合法的 JSON';
foreach ($data as $idx => $link) {
$link_name = trim($link['name']);
$link_url = trim($link['url']);
if ($link_name && $link_url) {
$new_data[] = [
'name' => $link_name,
'url' => $link_url,
];
}
}
$vdata['links'] = $new_data;
return '';
}, null);
$links_form->appendHTML(<<<EOD
<div id="div-contest_links"></div>
<input type="hidden" name="contest_links" id="input-contest_links" value="">
<script>
var contest_links = {$links_str};
var contest_links_cnt = Object.keys(contest_links).length;
$(document).ready(function() {
$('#input-contest_links').val(JSON.stringify(contest_links));
function newLinkRow(idx) {
var div_link = $('<div class="row mt-2" />');
var input_link_name = $('<input type="text" class="form-control" placeholder="名称" />').val(contest_links[idx].name);
var input_link_url = $('<input type="text" class="form-control" placeholder="链接" />').val(contest_links[idx].url);
var btn_del_cur_link = $('<button type="button" class="btn btn-sm btn-outline-secondary" />').html('<i class="bi bi-x-lg"></i>');
input_link_name.change(function() {
contest_links[idx].name = input_link_name.val();
$('#input-contest_links').val(JSON.stringify(contest_links));
});
input_link_url.change(function() {
contest_links[idx].url = input_link_url.val();
$('#input-contest_links').val(JSON.stringify(contest_links));
});
btn_del_cur_link.click(function() {
contest_links[idx] = undefined;
$('#input-contest_links').val(JSON.stringify(contest_links));
div_link.remove();
});
div_link.append(
$('<div class="col-11" />').append(
$('<div class="row" />').append(
$('<div class="col-md-6" />').append(input_link_name)
).append(
$('<div class="col-md-6" />').append(input_link_url)
)
)
).append(
$('<div class="col-1 text-center" />').append(btn_del_cur_link)
);
return div_link;
};
$.map(contest_links, function(link, idx) {
$('#div-contest_links').append(newLinkRow(idx));
});
var row_add_link = $('<div class="row mt-2 justify-content-end" />');
var btn_add_link = $('<button type="button" class="btn btn-sm btn-outline-secondary" />').html('<i class="bi bi-plus-lg"></i>');
btn_add_link.click(function() {
contest_links[++contest_links_cnt] = {name:'', url:''};
row_add_link.before(newLinkRow(contest_links_cnt));
});
$('#div-contest_links').append(row_add_link.append($('<div class="col-1 text-center" />').append(btn_add_link)));
});
</script>
EOD);
$links_form->setAjaxSubmit(<<<EOD
function(res) {
if (res.status === 'success') {
$('#links-result-alert')
.html('修改成功!')
.addClass('alert-success')
.removeClass('alert-danger')
.show();
} else {
$('#links-result-alert')
.html('修改失败。' + (res.message || ''))
.removeClass('alert-success')
.addClass('alert-danger')
.show();
}
setTimeout(function() {
$('#links-result-alert').hide();
}, 5000);
$(window).scrollTop(0);
}
EOD);
$links_form->handle = function (&$vdata) {
$extra_config = UOJContest::info('extra_config');
$extra_config['links'] = $vdata['links'];
$esc_extra_config = json_encode($extra_config);
DB::update([
"update contests",
"set", [
"extra_config" => $esc_extra_config,
],
"where", [
"id" => UOJContest::info('id'),
],
]);
dieWithJsonData(['status' => 'success', 'message' => '修改成功']);
};
$links_form->runAtServer();
} }
?> ?>
@ -527,6 +658,9 @@ EOD);
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" href="#type" data-bs-toggle="tab" data-bs-target="#type">规则</a> <a class="nav-link active" href="#type" data-bs-toggle="tab" data-bs-target="#type">规则</a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="#contest-links" data-bs-toggle="tab" data-bs-target="#contest-links">链接</a>
</li>
</ul> </ul>
</div> </div>
<div class="card-body tab-content"> <div class="card-body tab-content">
@ -545,11 +679,15 @@ EOD);
</ul> </ul>
<h5>常见问题</h5> <h5>常见问题</h5>
<ul class="mb-0"> <ul class="mb-0">
<li>暂无</li> <li>团体赛推荐使用团体账号报名参赛以解锁全部功能。</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane" id="contest-links">
<div id="links-result-alert" class="alert" role="alert" style="display: none"></div>
<?php $links_form->printHTML() ?>
</div>
</div> </div>
</div> </div>
<?php endif ?> <?php endif ?>

View File

@ -0,0 +1,32 @@
<?php
UOJContest::init(UOJRequest::get('id')) || UOJResponse::page404();
UOJContest::cur()->userCanView(Auth::user(), ['ensure' => true, 'check-register' => true]);
// Create directory if not exists
if (!is_dir(UOJContest::cur()->getResourcesPath())) {
mkdir(UOJContest::cur()->getResourcesPath(), 0755, true);
}
define('APP_TITLE', '比赛资源 - ' . UOJContest::info('title'));
define('FM_EMBED', true);
define('FM_DISABLE_COLS', true);
define('FM_DATETIME_FORMAT', UOJTime::FORMAT);
define('FM_ROOT_PATH', UOJContest::cur()->getResourcesFolderPath());
define('FM_ROOT_URL', UOJContest::cur()->getResourcesBaseUri());
$sub_path = UOJRequest::get('sub_path', 'is_string', '');
if ($sub_path) {
$filepath = realpath(UOJContest::cur()->getResourcesPath(rawurldecode($sub_path)));
$realbasepath = realpath(UOJContest::cur()->getResourcesPath());
if (!strStartWith($filepath, $realbasepath)) {
UOJResponse::page406();
}
UOJResponse::xsendfile($filepath);
}
$global_readonly = !UOJContest::cur()->userCanManage(Auth::user());
include(__DIR__ . '/tinyfilemanager/tinyfilemanager.php');

View File

@ -358,7 +358,7 @@ if (UOJContest::cur()) {
<div class="card card-default mb-2"> <div class="card card-default mb-2">
<div class="card-body"> <div class="card-body">
<h3 class="h4 card-title text-center"> <h3 class="h4 card-title text-center">
<a class="text-decoration-none text-body" href="/contest/<?= UOJContest::info('id') ?>"> <a class="text-decoration-none text-body" href="<?= UOJContest::cur()->getUri() ?>">
<?= UOJContest::info('name') ?> <?= UOJContest::info('name') ?>
</a> </a>
</h3> </h3>

View File

@ -1,11 +1,7 @@
<?php <?php
requireLib('hljs');
Auth::check() || redirectToLogin(); Auth::check() || redirectToLogin();
UOJProblem::init(UOJRequest::get('id')) || UOJResponse::page404(); UOJProblem::init(UOJRequest::get('id')) || UOJResponse::page404();
$problem = UOJProblem::cur()->info;
$problem_content = UOJProblem::cur()->queryContent();
$user_can_view = UOJProblem::cur()->userCanView(Auth::user()); $user_can_view = UOJProblem::cur()->userCanView(Auth::user());
if (!$user_can_view) { if (!$user_can_view) {

View File

@ -36,7 +36,9 @@ return [
'problem self review' => 'Problem self review', 'problem self review' => 'Problem self review',
'contest self review' => 'Contest self review', 'contest self review' => 'Contest self review',
'contest self reviews' => 'Contest self reviews', 'contest self reviews' => 'Contest self reviews',
'will start in x days' => function($x) { 'will start in x days' => function ($x) {
return "will start in $x ".($x > 1 ? "days" : "day"); return "will start in $x " . ($x > 1 ? "days" : "day");
}, },
'links' => 'Links',
'resources' => 'Resources',
]; ];

View File

@ -36,7 +36,9 @@ return [
'problem self review' => '题目总结', 'problem self review' => '题目总结',
'contest self review' => '比赛总结', 'contest self review' => '比赛总结',
'contest self reviews' => '赛后总结', 'contest self reviews' => '赛后总结',
'will start in x days' => function($x) { 'will start in x days' => function ($x) {
return "将在 $x 天后开始"; return "将在 $x 天后开始";
}, },
'links' => '链接',
'resources' => '相关资源',
]; ];

View File

@ -593,4 +593,24 @@ class UOJContest {
updateContestPlayerNum($this->info); updateContestPlayerNum($this->info);
return true; return true;
} }
public function getResourcesFolderPath() {
return UOJContext::storagePath() . "/contest_resources/" . $this->info['id'];
}
public function getResourcesPath($name = '') {
return "{$this->getResourcesFolderPath()}/$name";
}
public function getResourcesBaseUri() {
return "/contest/{$this->info['id']}/resources";
}
public function getResourcesUri($name = '') {
return "{$this->getResourcesBaseUri()}/{$name}";
}
public function getAdditionalLinks() {
return $this->info['extra_config']['links'] ?: [];
}
} }

View File

@ -42,13 +42,13 @@ Route::group(
Route::any('/contest/{id}/registrants', '/contest_members.php'); Route::any('/contest/{id}/registrants', '/contest_members.php');
Route::any('/contest/{id}/register', '/contest_registration.php'); Route::any('/contest/{id}/register', '/contest_registration.php');
Route::any('/contest/{id}/confirm', '/contest_confirmation.php'); Route::any('/contest/{id}/confirm', '/contest_confirmation.php');
Route::any('/contest/{id}/resources(?:/{sub_path})?', '/contest_resources.php');
Route::any('/contest/{id}/manage(?:/{tab})?', '/contest_manage.php'); Route::any('/contest/{id}/manage(?:/{tab})?', '/contest_manage.php');
Route::any('/contest/{id}/submissions', '/contest_inside.php?tab=submissions'); Route::any('/contest/{id}/submissions', '/contest_inside.php?tab=submissions');
Route::any('/contest/{id}/standings', '/contest_inside.php?tab=standings'); Route::any('/contest/{id}/standings', '/contest_inside.php?tab=standings');
Route::any('/contest/{id}/after_contest_standings', '/contest_inside.php?tab=after_contest_standings'); Route::any('/contest/{id}/after_contest_standings', '/contest_inside.php?tab=after_contest_standings');
Route::any('/contest/{id}/self_reviews', '/contest_inside.php?tab=self_reviews'); Route::any('/contest/{id}/self_reviews', '/contest_inside.php?tab=self_reviews');
Route::any('/contest/{id}/backstage', '/contest_inside.php?tab=backstage'); Route::any('/contest/{id}/backstage', '/contest_inside.php?tab=backstage');
Route::any('/contest/{id}/standings_unfrozen', '/contest_inside.php?tab=standings_unfrozen');
Route::any('/contest/{contest_id}/problem/{id}', '/problem.php'); Route::any('/contest/{contest_id}/problem/{id}', '/problem.php');
Route::any('/contest/{contest_id}/problem/{id}/statistics', '/problem_statistics.php'); Route::any('/contest/{contest_id}/problem/{id}/statistics', '/problem_statistics.php');

View File

@ -0,0 +1 @@
ALTER TABLE `contests` MODIFY `extra_config` json NOT NULL;

View File

@ -0,0 +1,37 @@
<?php
return function ($type) {
if ($type == 'up_after_sql') {
DB::init();
$contests = DB::selectAll("select * from contests");
foreach ($contests as $contest) {
$extra_config = json_decode($contest['extra_config'], true);
if (isset($extra_config['links'])) {
$new_links = [];
foreach ($extra_config['links'] as $link) {
if (isset($link['name'])) continue;
$new_links[] = [
'name' => $link[0],
'url' => '/blogs/' . $link[1],
];
}
$extra_config['links'] = $new_links;
}
DB::update([
"update contests",
"set", [
"extra_config" => json_encode($extra_config, JSON_FORCE_OBJECT),
],
"where", [
"id" => $contest['id'],
],
]);
}
}
};

View File

@ -83,6 +83,7 @@ initProgress(){
mkdir -p /opt/uoj/web/app/storage/tmp mkdir -p /opt/uoj/web/app/storage/tmp
mkdir -p /opt/uoj/web/app/storage/image_hosting mkdir -p /opt/uoj/web/app/storage/image_hosting
mkdir -p /opt/uoj/web/app/storage/problem_resources mkdir -p /opt/uoj/web/app/storage/problem_resources
mkdir -p /opt/uoj/web/app/storage/contest_resources
chmod -R 777 /opt/uoj/web/app/storage chmod -R 777 /opt/uoj/web/app/storage
#Using cli upgrade to latest #Using cli upgrade to latest
sleep 15 # Wait for uoj-db sleep 15 # Wait for uoj-db