ui: update acm scoreboard display

This commit is contained in:
undefined committed 2022-03-25 10:53:14 +08:00
1 parent 9f796e7024
commit 3ec549ee9c
12 files changed
+86 -88

No files matched your search

+1
View File
@@ -542,6 +542,7 @@ Password: 密码
Passwords don't match.: 验证密码不匹配。 Passwords don't match.: 验证密码不匹配。
Path {0} not found.: 路径 {0} 不存在。 Path {0} not found.: 路径 {0} 不存在。
Peak Memory: 峰值内存 Peak Memory: 峰值内存
Penalty (Minutes): 罚时(分钟)
perm_contest: 比赛 perm_contest: 比赛
perm_discussion: 讨论 perm_discussion: 讨论
perm_general: 一般 perm_general: 一般
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hydrooj", "name": "hydrooj",
"version": "3.10.7", "version": "3.11.0",
"bin": "bin/hydrooj.js", "bin": "bin/hydrooj.js",
"main": "src/loader", "main": "src/loader",
"module": "src/loader", "module": "src/loader",
+5 -1
View File
@@ -183,6 +183,10 @@ export class ContestScoreboardHandler extends Handler {
if (ignoreLock && !this.user.own(tdoc)) { if (ignoreLock && !this.user.own(tdoc)) {
this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
} }
const pdict = await problem.getList(domainId, tdoc.pids, true, undefined, false, [
// Problem statistics display is allowed as we can view submission info in scoreboard.
...problem.PROJECTION_CONTEST_LIST, 'nSubmit', 'nAccept',
]);
const [, rows, udict, , nPages] = await contest.getScoreboard.call(this, domainId, tid, false, page, ignoreLock); const [, rows, udict, , nPages] = await contest.getScoreboard.call(this, domainId, tid, false, page, ignoreLock);
const path = [ const path = [
['Hydro', 'homepage'], ['Hydro', 'homepage'],
@@ -192,7 +196,7 @@ export class ContestScoreboardHandler extends Handler {
]; ];
this.response.template = 'contest_scoreboard.html'; this.response.template = 'contest_scoreboard.html';
this.response.body = { this.response.body = {
tdoc, rows, path, udict, nPages, page, tdoc, rows, path, udict, nPages, page, pdict,
}; };
} }
} }
+2 -1
View File
@@ -304,11 +304,12 @@ export interface RecordDoc {
} }
export interface ScoreboardNode { export interface ScoreboardNode {
type: string; type: 'string' | 'rank' | 'user' | 'record' | 'records' | 'problem' | 'solved' | 'time' | 'total_score';
value: string; value: string;
raw?: any; raw?: any;
score?: number; score?: number;
style?: string; style?: string;
hover?: string;
} }
export type ScoreboardRow = ScoreboardNode[] & { raw?: any }; export type ScoreboardRow = ScoreboardNode[] & { raw?: any };
+46 -66
View File
@@ -84,37 +84,27 @@ const acm = buildContestRule({
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids); const udict = await user.getList(tdoc.domainId, uids);
const columns: ScoreboardRow = [ const columns: ScoreboardRow = [
{ type: 'rank', value: _('Rank') }, { type: 'rank', value: '#' },
{ type: 'user', value: _('User') }, { type: 'user', value: _('User') },
{ type: 'solved_problems', value: _('Solved') }, { type: 'solved', value: `${_('Solved')}\n${_('Total Time')}` },
]; ];
if (isExport) {
columns.push(
{ type: 'total_time', value: _('Penalty') },
{ type: 'total_time', value: _('Total Time (Seconds)') },
);
}
for (let i = 1; i <= tdoc.pids.length; i++) { for (let i = 1; i <= tdoc.pids.length; i++) {
const pid = tdoc.pids[i - 1]; const pid = tdoc.pids[i - 1];
if (isExport) { if (isExport) {
columns.push( columns.push(
{ {
type: 'problem_flag', type: 'string',
value: '#{0} {1}'.format(i, pdict[pid].title), value: '#{0} {1}'.format(i, pdict[pid].title),
}, },
{ {
type: 'problem_time', type: 'time',
value: '#{0} {1}'.format(i, _('Time (Seconds)')), value: '#{0} {1}'.format(i, _('Penalty (Minutes)')),
},
{
type: 'problem_time_str',
value: '#{0} {1}'.format(i, _('Time')),
}, },
); );
} else { } else {
columns.push({ columns.push({
type: 'problem_detail', type: 'problem',
value: '#{0}'.format(i), value: String.fromCharCode(65 + i - 1),
raw: pid, raw: pid,
}); });
} }
@@ -144,37 +134,36 @@ const acm = buildContestRule({
const tsddict: Record<number, AcmDetail> = {}; const tsddict: Record<number, AcmDetail> = {};
for (const item of tsdoc.detail || []) tsddict[item.pid] = item; for (const item of tsdoc.detail || []) tsddict[item.pid] = item;
const row: ScoreboardRow = [ const row: ScoreboardRow = [
{ type: 'string', value: rank.toString() }, { type: 'rank', value: rank.toString() },
{ type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid }, { type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid },
{ type: 'string', value: tsdoc.accept || 0 }, {
type: 'time',
value: `${tsdoc.accept || 0}\n${misc.formatSeconds(tsdoc.time || 0.0, false)}`,
hover: misc.formatSeconds(tsdoc.time || 0.0),
},
]; ];
if (isExport) {
const penalty = Math.sum(tdoc.pids.map((i) => tsddict[i]?.naccept || 0)) * 20 * 60;
row.push(
{ type: 'string', value: penalty.toString() },
{ type: 'string', value: tsdoc.time || 0.0 },
);
}
for (const pid of tdoc.pids) { for (const pid of tdoc.pids) {
const doc = tsddict[pid] || {} as Partial<AcmDetail>; const doc = tsddict[pid] || {} as Partial<AcmDetail>;
const accept = doc.status === STATUS.STATUS_ACCEPTED; const accept = doc.status === STATUS.STATUS_ACCEPTED;
const rid = accept ? doc.rid : null; const colTime = accept ? misc.formatSeconds(doc.real, false).toString() : '';
const colAccepted = `${(accept && isExport) ? `${_('Accepted')} ` : ''}${doc.naccept ? ` (-${doc.naccept})` : ''}`; const colPenalty = doc.rid ? Math.ceil(doc.penalty / 60).toString() : '';
const colTime = accept ? doc.time.toString() : '-';
const colTimeStr = accept ? misc.formatSeconds(doc.time) : '-';
if (isExport) { if (isExport) {
row.push( row.push(
{ type: 'string', value: colAccepted },
{ type: 'string', value: colTime }, { type: 'string', value: colTime },
{ type: 'string', value: colTimeStr }, { type: 'string', value: colPenalty },
); );
} else { } else {
row.push({ row.push({
type: 'record', type: 'record',
score: accept ? 100 : 0, score: accept ? 100 : 0,
value: '{0}\n{1}'.format(colAccepted, colTimeStr), value: accept
raw: rid, ? `${colTime}\n<span style="font-weight:normal;color:#000">${doc.penalty ? `+ (-${doc.naccept})` : ''}</span>`
style: accept && rid.generationTime === first[pid] : doc.rid
? `-${doc.naccept}`
: '',
hover: accept ? misc.formatSeconds(doc.time) : '',
raw: doc.rid,
style: accept && doc.rid.generationTime === first[pid]
? 'background-color: rgb(217, 240, 199);' ? 'background-color: rgb(217, 240, 199);'
: undefined, : undefined,
}); });
@@ -211,20 +200,20 @@ const oi = buildContestRule({
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids); const udict = await user.getList(tdoc.domainId, uids);
const columns: ScoreboardNode[] = [ const columns: ScoreboardNode[] = [
{ type: 'rank', value: _('Rank') }, { type: 'rank', value: '#' },
{ type: 'user', value: _('User') }, { type: 'user', value: _('User') },
{ type: 'total_score', value: _('Total Score') }, { type: 'total_score', value: _('Total Score') },
]; ];
for (let i = 1; i <= tdoc.pids.length; i++) { for (let i = 1; i <= tdoc.pids.length; i++) {
if (isExport) { if (isExport) {
columns.push({ columns.push({
type: 'problem_score', type: 'string',
value: '#{0} {1}'.format(i, pdict[tdoc.pids[i - 1]].title), value: '#{0} {1}'.format(i, pdict[tdoc.pids[i - 1]].title),
}); });
} else { } else {
columns.push({ columns.push({
type: 'problem_detail', type: 'problem',
value: '#{0}'.format(i), value: String.fromCharCode(65 + i - 1),
raw: tdoc.pids[i - 1], raw: tdoc.pids[i - 1],
}); });
} }
@@ -251,17 +240,17 @@ const oi = buildContestRule({
} }
} }
} }
const rows = [columns]; const rows: ScoreboardNode[][] = [columns];
for (const [rank, tsdoc] of rankedTsdocs) { for (const [rank, tsdoc] of rankedTsdocs) {
const tsddict = {}; const tsddict = {};
if (tsdoc.journal) { if (tsdoc.journal) {
for (const item of tsdoc.journal) tsddict[item.pid] = item; for (const item of tsdoc.journal) tsddict[item.pid] = item;
} }
const row = []; const row: ScoreboardNode[] = [];
row.push( row.push(
{ type: 'string', value: rank }, { type: 'string', value: rank.toString() },
{ type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid }, { type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid },
{ type: 'string', value: tsdoc.score || 0 }, { type: 'total_score', value: tsdoc.score || 0 },
); );
for (const pid of tdoc.pids) { for (const pid of tdoc.pids) {
const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`; const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`;
@@ -356,44 +345,37 @@ const homework = buildContestRule({
async scoreboard(isExport, _, tdoc, pdict, cursor, page) { async scoreboard(isExport, _, tdoc, pdict, cursor, page) {
const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score, page); const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score, page);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList.call(this, tdoc.domainId, uids); const udict = await user.getList(tdoc.domainId, uids);
const columns: ScoreboardNode[] = [ const columns: ScoreboardNode[] = [
{ type: 'rank', value: _('Rank') }, { type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') }, { type: 'user', value: _('User') },
{ type: 'total_score', value: _('Score') }, { type: 'total_score', value: _('Score') },
]; ];
if (isExport) { if (isExport) {
columns.push( columns.push({ type: 'string', value: _('Original Score') });
{ type: 'total_original_score', value: _('Original Score') },
{ type: 'total_time', value: _('Total Time (Seconds)') },
);
} }
columns.push({ type: 'total_time_str', value: _('Total Time') }); columns.push({ type: 'time', value: _('Total Time') });
for (let i = 1; i <= tdoc.pids.length; i++) { for (let i = 1; i <= tdoc.pids.length; i++) {
const pid = tdoc.pids[i - 1]; const pid = tdoc.pids[i - 1];
if (isExport) { if (isExport) {
columns.push( columns.push(
{ {
type: 'problem_score', type: 'string',
value: '#{0} {1}'.format(i, pdict[pid].title), value: '#{0} {1}'.format(i, pdict[pid].title),
}, },
{ {
type: 'problem_original_score', type: 'string',
value: '#{0} {1}'.format(i, _('Original Score')), value: '#{0} {1}'.format(i, _('Original Score')),
}, },
{ {
type: 'problem_time', type: 'time',
value: '#{0} {1}'.format(i, _('Time (Seconds)')), value: '#{0} {1}'.format(i, _('Time (Seconds)')),
}, },
{
type: 'problem_time_str',
value: '#{0} {1}'.format(i, _('Time')),
},
); );
} else { } else {
columns.push({ columns.push({
type: 'problem_detail', type: 'problem',
value: '#{0}'.format(i), value: String.fromCharCode(65 + i - 1),
raw: pid, raw: pid,
}); });
} }
@@ -418,21 +400,19 @@ const homework = buildContestRule({
]; ];
if (isExport) { if (isExport) {
row.push({ type: 'string', value: tsdoc.score || 0 }); row.push({ type: 'string', value: tsdoc.score || 0 });
row.push({ type: 'string', value: tsdoc.time || 0.0 });
} }
row.push({ type: 'string', value: misc.formatSeconds(tsdoc.time || 0) }); row.push({ type: 'time', value: misc.formatSeconds(tsdoc.time || 0, false), raw: tsdoc.time });
for (const pid of tdoc.pids) { for (const pid of tdoc.pids) {
const rid = tsddict[pid]?.rid; const rid = tsddict[pid]?.rid;
const colScore = tsddict[pid]?.penaltyScore || '-'; const colScore = tsddict[pid]?.penaltyScore || '';
const colOriginalScore = tsddict[pid]?.score || '-'; const colOriginalScore = tsddict[pid]?.score || '';
const colTime = tsddict[pid]?.time || '-'; const colTime = tsddict[pid]?.time || '';
const colTimeStr = colTime !== '-' ? misc.formatSeconds(colTime) : '-'; const colTimeStr = colTime ? misc.formatSeconds(colTime, false) : '';
if (isExport) { if (isExport) {
row.push( row.push(
{ type: 'string', value: colScore }, { type: 'string', value: colScore },
{ type: 'string', value: colOriginalScore }, { type: 'string', value: colOriginalScore },
{ type: 'string', value: colTime }, { type: 'time', value: colTime },
{ type: 'string', value: colTimeStr },
); );
} else { } else {
row.push({ row.push({
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/ui-default", "name": "@hydrooj/ui-default",
"version": "4.34.23", "version": "4.34.24",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "hydro.js", "main": "hydro.js",
@@ -1,23 +1,32 @@
.page--contest_scoreboard .page--contest_scoreboard
.col--rank .col--rank
width: rem(60px) width: rem(45px)
text-align: center
border-right: 1px solid $table-border-color border-right: 1px solid $table-border-color
.col--user .col--user
min-width: rem(150px) min-width: rem(150px)
.col--total_score, .col--total_time_str, .col--solved_problems .col--solved, .col--time
width: rem(70px)
text-align: center
.col--total_score
width: rem(100px) width: rem(100px)
text-align: center text-align: center
.col--problem_detail .col--problem
width: rem(100px) width: rem(100px)
border-left: 1px solid $table-border-color border-left: 1px solid $table-border-color
text-align: center text-align: center
.scoreboard--acm
.col--problem
width: rem(70px) !important
+mobile() +mobile()
.col--user .col--user
width: auto width: auto
.col--problem_detail .col--problem
display: none display: none
@@ -6,11 +6,11 @@
.col--user .col--user
width: rem(140px) width: rem(140px)
.col--total_score, .col--total_time_str, .col--solved_problems .col--total_score
width: rem(100px) width: rem(100px)
text-align: center text-align: center
.col--problem_detail .col--problem, .col--record
min-width: rem(100px) min-width: rem(100px)
border-left: 1px solid $table-border-color border-left: 1px solid $table-border-color
text-align: center text-align: center
@@ -19,5 +19,5 @@
.col--user .col--user
width: auto width: auto
.col--problem_detail .col--problem, .col--record
display: none display: none
@@ -2,7 +2,7 @@
{% extends "layout/basic.html" %} {% extends "layout/basic.html" %}
{% set type = 'homework' if tdoc.docType == 60 else 'contest' %} {% set type = 'homework' if tdoc.docType == 60 else 'contest' %}
{% block content %} {% block content %}
<div class="row"><div class="medium-12 columns"> <div class="row"><div class="medium-12 columns scoreboard--{{ tdoc.rule }}">
<div class="section visible"> <div class="section visible">
<div class="section__header"> <div class="section__header">
<a class="button" target="_blank" href="{{ url(type + '_scoreboard_download', tid=tdoc.docId, ext='html') }}"> <a class="button" target="_blank" href="{{ url(type + '_scoreboard_download', tid=tdoc.docId, ext='html') }}">
@@ -30,10 +30,12 @@
<tr> <tr>
{%- for column in rows[0] -%} {%- for column in rows[0] -%}
<th class="col--{{ column.type }}"> <th class="col--{{ column.type }}">
{% if column.type == 'problem_detail' %} {% if column.type == 'problem' %}
<a href="{{ url('problem_detail', pid=column.raw, query={tid:tdoc.docId}) }}" data-tooltip="{{ pdict[column.raw].title }}">{{ column.value }}</a> <a href="{{ url('problem_detail', pid=column.raw, query={tid:tdoc.docId}) }}" data-tooltip="{{ pdict[column.raw].title }}">
{{ column.value }}<br />{{ pdict[column.raw].nAccept }}/{{ pdict[column.raw].nSubmit }}
</a>
{% else %} {% else %}
{{ column.value }} {{ column.value|replaceBr|safe }}
{% endif %} {% endif %}
</th> </th>
{%- endfor -%} {%- endfor -%}
@@ -49,7 +51,7 @@
{% set canView = canViewAll or handler.user._id == column.raw %} {% set canView = canViewAll or handler.user._id == column.raw %}
{{ user.render_inline(udict[column.raw], badge=false) }} {{ user.render_inline(udict[column.raw], badge=false) }}
{% elif column.type == 'record' and column.raw %} {% elif column.type == 'record' and column.raw %}
<a href="{{ url('record_detail', rid=column.raw) if canView else '' }}"> <a href="{{ url('record_detail', rid=column.raw) if canView else '' }}" data-tooltip="{{ column.hover }}">
<span style="font-weight:bold;color:{{ utils.status.getScoreColor(column.score|default(column.value)) }}">{{ column.value|replaceBr|safe }}</span> <span style="font-weight:bold;color:{{ utils.status.getScoreColor(column.score|default(column.value)) }}">{{ column.value|replaceBr|safe }}</span>
</a> </a>
{% elif column.type == 'records' %} {% elif column.type == 'records' %}
+1 -1
View File
@@ -150,7 +150,7 @@
<h1 class="section__title" id="upload" data-heading>{{ _('Dataset Format') }}</h1> <h1 class="section__title" id="upload" data-heading>{{ _('Dataset Format') }}</h1>
</div> </div>
<div class="section__body typo"> <div class="section__body typo">
<p>详见<a href="https://hydro.js.org/docs/testdata.html">配置文档</a></p> <p>详见<a href="https://hydro.js.org/docs/user/testdata/">配置文档</a></p>
</div> </div>
</div> </div>
<div class="section" data-heading-extract-to="#menu-item-wiki_help"> <div class="section" data-heading-extract-to="#menu-item-wiki_help">
+5 -4
View File
@@ -305,13 +305,14 @@ function _digit2(number: number) {
return number < 10 ? `0${number}` : number.toString(); return number < 10 ? `0${number}` : number.toString();
} }
export function formatSeconds(_seconds: string | number = '0') { export function formatSeconds(_seconds: string | number = '0', showSeconds = true) {
const seconds = +_seconds; const seconds = +_seconds;
return '{0}:{1}:{2}'.format( let res = '{0}:{1}'.format(
_digit2(Math.floor(seconds / 3600)), showSeconds ? _digit2(Math.floor(seconds / 3600)) : Math.floor(seconds / 3600),
_digit2(Math.floor((seconds % 3600) / 60)), _digit2(Math.floor((seconds % 3600) / 60)),
_digit2(seconds % 60),
); );
if (showSeconds) res += `:${_digit2(seconds % 60)}`;
return res;
} }
export function changeErrorType(err: any, Err: any) { export function changeErrorType(err: any, Err: any) {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/utils", "name": "@hydrooj/utils",
"version": "1.1.15", "version": "1.1.16",
"description": "hydrooj utils", "description": "hydrooj utils",
"main": "package.json", "main": "package.json",
"repository": "https://github.com/hydro-dev/Hydro.git", "repository": "https://github.com/hydro-dev/Hydro.git",