Compare commits

..

4 Commits

Author SHA1 Message Date
9c2b2a96ab
feat: contest self reviews
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-19 19:27:57 +08:00
419be8ab49
fix: bb5de2a00e 2022-09-19 19:06:16 +08:00
46f6923d97
fix: command 2022-09-19 17:37:02 +08:00
62b6c3c182
chore: mount uoj_judger/data 2022-09-19 16:58:09 +08:00
11 changed files with 109 additions and 15 deletions

View File

@ -35,7 +35,7 @@ docker-compose up -d
## 开发 ## 开发
```bash ```bash
docker-compose up --build -f docker-compose.development.yml docker-compose -f docker-compose.development.yml up --build
``` ```
## 感谢 ## 感谢

View File

@ -269,6 +269,7 @@ UNLOCK TABLES;
CREATE TABLE `contests_problems` ( CREATE TABLE `contests_problems` (
`problem_id` int(11) NOT NULL, `problem_id` int(11) NOT NULL,
`contest_id` int(11) NOT NULL, `contest_id` int(11) NOT NULL,
`dfn` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`problem_id`,`contest_id`) PRIMARY KEY (`problem_id`,`contest_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
@ -332,6 +333,30 @@ LOCK TABLES `contests_submissions` WRITE;
/*!40000 ALTER TABLE `contests_submissions` ENABLE KEYS */; /*!40000 ALTER TABLE `contests_submissions` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
--
-- Table structure for table `contests_reviews`
--
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `contests_reviews` (
`contest_id` int(11) NOT NULL,
`problem_id` int(11) NOT NULL DEFAULT 0,
`poster` varchar(20) NOT NULL,
`content` text NOT NULL,
PRIMARY KEY (`contest_id`,`problem_id`,`poster`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `contests_reviews`
--
LOCK TABLES `contests_reviews` WRITE;
/*!40000 ALTER TABLE `contests_reviews` DISABLE KEYS */;
/*!40000 ALTER TABLE `contests_reviews` ENABLE KEYS */;
UNLOCK TABLES;
-- --
-- Table structure for table `countdowns` -- Table structure for table `countdowns`
-- --

View File

@ -20,6 +20,7 @@ services:
- SYS_PTRACE - SYS_PTRACE
volumes: volumes:
- ./uoj_data/judger/log:/opt/uoj_judger/log - ./uoj_data/judger/log:/opt/uoj_judger/log
- ./uoj_data/judger/data:/opt/uoj_judger/uoj_judger/data
environment: environment:
- UOJ_PROTOCOL=http - UOJ_PROTOCOL=http
- UOJ_HOST=uoj-web - UOJ_HOST=uoj-web

View File

@ -44,11 +44,18 @@
'name' => UOJLocale::get('contests::contest standings'), 'name' => UOJLocale::get('contests::contest standings'),
'url' => "/contest/{$contest['id']}/standings" 'url' => "/contest/{$contest['id']}/standings"
), ),
'after_contest_standings' => array( );
if ($contest['cur_progress'] > CONTEST_TESTING) {
$tabs_info['after_contest_standings'] = array(
'name' => UOJLocale::get('contests::after contest standings'), 'name' => UOJLocale::get('contests::after contest standings'),
'url' => "/contest/{$contest['id']}/after_contest_standings" 'url' => "/contest/{$contest['id']}/after_contest_standings"
)
); );
$tabs_info['self_reviews'] = array(
'name' => UOJLocale::get('contests::contest self reviews'),
'url' => "/contest/{$contest['id']}/self_reviews"
);
}
if (hasContestPermission(Auth::user(), $contest)) { if (hasContestPermission(Auth::user(), $contest)) {
$tabs_info['backstage'] = array( $tabs_info['backstage'] = array(
@ -392,6 +399,21 @@ EOD;
]); ]);
} }
function echoReviews() {
global $contest;
$contest_data = queryContestData($contest, array());
calcStandings($contest, $contest_data, $score, $standings, false, true);
uojIncludeView('contest-standings', [
'contest' => $contest,
'standings' => $standings,
'score' => $score,
'contest_data' => $contest_data,
'show_self_reviews' => true
]);
}
function echoContestCountdown() { function echoContestCountdown() {
global $contest; global $contest;
$rest_second = $contest['end_time']->getTimestamp() - UOJTime::$time_now->getTimestamp(); $rest_second = $contest['end_time']->getTimestamp() - UOJTime::$time_now->getTimestamp();
@ -454,7 +476,7 @@ EOD;
<?= getClickZanBlock('C', $contest['id'], $contest['zan']) ?> <?= getClickZanBlock('C', $contest['id'], $contest['zan']) ?>
</div> </div>
<div class="row"> <div class="row">
<?php if ($cur_tab == 'standings' || $cur_tab == 'after_contest_standings'): ?> <?php if ($cur_tab == 'standings' || $cur_tab == 'after_contest_standings' || $cur_tab == 'self_reviews'): ?>
<div class="col-sm-12"> <div class="col-sm-12">
<?php else: ?> <?php else: ?>
<div class="col-sm-9"> <div class="col-sm-9">
@ -472,12 +494,14 @@ EOD;
echoStandings(true); echoStandings(true);
} elseif ($cur_tab == 'backstage') { } elseif ($cur_tab == 'backstage') {
echoBackstage(); echoBackstage();
} elseif ($cur_tab == 'self_reviews') {
echoReviews();
} }
?> ?>
</div> </div>
</div> </div>
<?php if ($cur_tab == 'standings' || $cur_tab == 'after_contest_standings'): ?> <?php if ($cur_tab == 'standings' || $cur_tab == 'after_contest_standings' || $cur_tab == 'self_reviews'): ?>
<div class="col-sm-12"> <div class="col-sm-12">
<hr /> <hr />
</div> </div>

View File

@ -67,6 +67,7 @@
}, },
function($type, $username) { function($type, $username) {
global $contest; global $contest;
if ($type == '+') { if ($type == '+') {
DB::query("insert into contests_permissions (contest_id, username) values (${contest['id']}, '$username')"); DB::query("insert into contests_permissions (contest_id, username) values (${contest['id']}, '$username')");
} elseif ($type == '-') { } elseif ($type == '-') {
@ -99,7 +100,8 @@
$problem_id = $matches[1]; $problem_id = $matches[1];
if ($type == '+') { if ($type == '+') {
DB::insert("insert into contests_problems (contest_id, problem_id) values ({$contest['id']}, '$problem_id')"); $dfn = DB::selectFirst("select max(dfn) from contests_problems where contest_id = {$contest['id']}")['max(dfn)'] + 1;
DB::insert("insert into contests_problems (contest_id, problem_id, dfn) values ({$contest['id']}, '$problem_id', $dfn)");
} elseif ($type == '-') { } elseif ($type == '-') {
DB::delete("delete from contests_problems where contest_id = {$contest['id']} and problem_id = '$problem_id'"); DB::delete("delete from contests_problems where contest_id = {$contest['id']} and problem_id = '$problem_id'");
} }
@ -216,7 +218,7 @@
</thead> </thead>
<tbody> <tbody>
<?php <?php
$result = DB::query("select problem_id from contests_problems where contest_id = ${contest['id']} order by dfn asc"); $result = DB::query("select problem_id from contests_problems where contest_id = ${contest['id']} order by dfn, problem_id");
while ($row = DB::fetch($result, MYSQLI_ASSOC)) { while ($row = DB::fetch($result, MYSQLI_ASSOC)) {
$problem = queryProblemBrief($row['problem_id']); $problem = queryProblemBrief($row['problem_id']);
$problem_config_str = isset($contest['extra_config']["problem_{$problem['id']}"]) ? $contest['extra_config']["problem_{$problem['id']}"] : 'sample'; $problem_config_str = isset($contest['extra_config']["problem_{$problem['id']}"]) ? $contest['extra_config']["problem_{$problem['id']}"] : 'sample';

View File

@ -46,7 +46,7 @@ function queryContestData($contest, $config = array(), $is_after_contest_query =
$problems = []; $problems = [];
$prob_pos = []; $prob_pos = [];
$n_problems = 0; $n_problems = 0;
$result = DB::query("select problem_id from contests_problems where contest_id = {$contest['id']} order by dfn"); $result = DB::query("select problem_id from contests_problems where contest_id = {$contest['id']} order by dfn, problem_id");
while ($row = DB::fetch($result, MYSQLI_NUM)) { while ($row = DB::fetch($result, MYSQLI_NUM)) {
$prob_pos[$problems[] = (int)$row[0]] = $n_problems++; $prob_pos[$problems[] = (int)$row[0]] = $n_problems++;
} }
@ -92,8 +92,8 @@ function queryContestData($contest, $config = array(), $is_after_contest_query =
return ['problems' => $problems, 'data' => $data, 'people' => $people]; return ['problems' => $problems, 'data' => $data, 'people' => $people];
} }
function calcStandings($contest, $contest_data, &$score, &$standings, $update_contests_submissions = false) { function calcStandings($contest, $contest_data, &$score, &$standings, $update_contests_submissions = false, $show_reviews = false) {
// score: username, problem_pos => score, penalty, id // score: username, problem_pos => score, penalty, id, ?review
$score = array(); $score = array();
$n_people = count($contest_data['people']); $n_people = count($contest_data['people']);
$n_problems = count($contest_data['problems']); $n_problems = count($contest_data['problems']);
@ -107,10 +107,19 @@ function calcStandings($contest, $contest_data, &$score, &$standings, $update_co
$penalty = 0; $penalty = 0;
} }
} }
$score[$submission[2]][$submission[3]] = array($submission[4], $penalty, $submission[0]); $score[$submission[2]][$submission[3]] = array($submission[4], $penalty, $submission[0]);
if ($show_reviews) {
$review_result = DB::selectFirst("select content from contests_reviews where contest_id = {$contest['id']} and problem_id = {$contest_data['problems'][$submission[3]]} and poster = '{$person[0]}'");
if ($review_result['content']) {
$score[$submission[2]][$submission[3]][] = $review_result['content'];
}
}
} }
// standings: rank => score, penalty, [username, realname], virtual_rank // standings: rank => score, penalty, [username, realname], virtual_rank, ?review
$standings = array(); $standings = array();
foreach ($contest_data['people'] as $person) { foreach ($contest_data['people'] as $person) {
$cur = array(0, 0, $person); $cur = array(0, 0, $person);
@ -124,6 +133,15 @@ function calcStandings($contest, $contest_data, &$score, &$standings, $update_co
} }
} }
} }
if ($show_reviews) {
$review_result = DB::selectFirst("select content from contests_reviews where contest_id = {$contest['id']} and poster = '{$person[0]}'");
if ($review_result['content']) {
$cur[] = $review_result['content'];
}
}
$standings[] = $cur; $standings[] = $cur;
} }
@ -143,8 +161,10 @@ function calcStandings($contest, $contest_data, &$score, &$standings, $update_co
for ($i = 0; $i < $n_people; $i++) { for ($i = 0; $i < $n_people; $i++) {
if ($i == 0 || !$is_same_rank($standings[$i - 1], $standings[$i])) { if ($i == 0 || !$is_same_rank($standings[$i - 1], $standings[$i])) {
$standings[$i][] = $i + 1; $standings[$i][] = $standings[$i][3];
$standings[$i][3] = $i + 1;
} else { } else {
$standings[$i][] = $standings[$i][3];
$standings[$i][] = $standings[$i - 1][3]; $standings[$i][] = $standings[$i - 1][3];
} }
} }

View File

@ -26,5 +26,6 @@ return [
'contest pending final test' => 'Pending final test', 'contest pending final test' => 'Pending final test',
'contest final testing' => 'Final testing', 'contest final testing' => 'Final testing',
'contest ended' => 'Contest Ended', 'contest ended' => 'Contest Ended',
'contest registrants' => 'Registrants' 'contest registrants' => 'Registrants',
'contest self reviews' => 'Contest self reviews'
]; ];

View File

@ -26,5 +26,6 @@ return [
'contest pending final test' => '等待评测', 'contest pending final test' => '等待评测',
'contest final testing' => '正在测评', 'contest final testing' => '正在测评',
'contest ended' => '比赛已结束', 'contest ended' => '比赛已结束',
'contest registrants' => '报名选手列表' 'contest registrants' => '报名选手列表',
'contest self reviews' => '赛后总结'
]; ];

View File

@ -31,6 +31,7 @@ Route::group([
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}/backstage', '/contest_inside.php?tab=backstage'); Route::any('/contest/{id}/backstage', '/contest_inside.php?tab=backstage');
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

@ -6,6 +6,7 @@
<script type="text/javascript"> <script type="text/javascript">
standings_version=<?=$contest['extra_config']['standings_version']?>; standings_version=<?=$contest['extra_config']['standings_version']?>;
show_self_reviews=<?=isset($show_self_reviews) && $show_self_reviews ? 'true' : 'false' ?>;
contest_id=<?=$contest['id']?>; contest_id=<?=$contest['id']?>;
standings=<?=json_encode($standings)?>; standings=<?=json_encode($standings)?>;
score=<?=json_encode($score)?>; score=<?=json_encode($score)?>;

View File

@ -1071,6 +1071,7 @@ function showStandings() {
$.map(problems, function(col, idx) { $.map(problems, function(col, idx) {
return '<th style="width:8em;">' + '<a href="/contest/' + contest_id + '/problem/' + col + '">' + String.fromCharCode('A'.charCodeAt(0) + idx) + '</a>' + '</th>'; return '<th style="width:8em;">' + '<a href="/contest/' + contest_id + '/problem/' + col + '">' + String.fromCharCode('A'.charCodeAt(0) + idx) + '</a>' + '</th>';
}).join('') + }).join('') +
(show_self_reviews ? '<th style="width:16em;">赛后总结</th>' : '') +
'</tr>', '</tr>',
function(row) { function(row) {
var col_tr = '<tr>'; var col_tr = '<tr>';
@ -1089,9 +1090,26 @@ function showStandings() {
col_tr += '<div>' + getPenaltyTimeStr(col[1]) + '</div>'; col_tr += '<div>' + getPenaltyTimeStr(col[1]) + '</div>';
} }
} }
if (show_self_reviews) {
col_tr += '<div id="review-' + row[2][0] + '-' + i + '"></div>'
+ '<script>'
+ '$(function() {'
+ 'var purify_result = DOMPurify.sanitize(\'' + String(col[3] || '').replace(/'/g, '\\\'').replace(new RegExp('</scr' + 'ipt>', 'gi'), '</scr\' + \'ipt>') + '\', {ALLOWED_TAGS: ["a", "b", "i", "u", "em", "strong", "sub", "sup", "small", "del"], ALLOWED_ATTR: ["href"]});'
+ '$("#review-' + row[2][0] + '-' + i + '")'
+ '.html(purify_result ? \'<div class="mt-3 pt-2 border-top">\' + purify_result + \'</div>\' : \'\'); });'
+ '</scr' + 'ipt>';
}
} }
col_tr += '</td>'; col_tr += '</td>';
} }
if (show_self_reviews) {
col_tr += '<td><div id="review-' + row[2][0] + '"></div>'
+ '<script>'
+ '$(function() { $("#review-' + row[2][0] + '")'
+ '.html(DOMPurify.sanitize(\'' + String(row[4] || '').replace(/'/g, '\\\'').replace(new RegExp('</scr' + 'ipt>', 'gi'), '</scr\' + \'ipt>') + '\', {ALLOWED_TAGS: ["a", "b", "i", "u", "em", "strong", "sub", "sup", "small", "del"], ALLOWED_ATTR: ["href"]})); });'
+ '</scr' + 'ipt></td>';
}
col_tr += '</tr>'; col_tr += '</tr>';
return col_tr; return col_tr;
}, { }, {