core: contest: support realtime scoreboard after freeze contest (#544)

Co-authored-by: panda <panda_dtdyy@outlook.com>
This commit is contained in:
undefinedandpanda authored and GitHub committed 2023-03-22 23:22:49 +08:00
1 parent e233bbb4cf
commit 17cf1194de
7 files changed
+96 -82

No files matched your search

+12 -4
View File
@@ -11,7 +11,7 @@ import {
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError,
InvalidTokenError, NotAssignedError, PermissionError, ValidationError,
} from '../error';
import { Tdoc } from '../interface';
import { ScoreboardConfig, Tdoc } from '../interface';
import paginate from '../lib/paginate';
import { PERM, PRIV, STATUS } from '../model/builtin';
import * as contest from '../model/contest';
@@ -265,13 +265,21 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler {
export class ContestScoreboardHandler extends ContestDetailBaseHandler {
@param('tid', Types.ObjectId)
@param('ext', Types.Range(['csv', 'html', 'ghost']), true)
async get(domainId: string, tid: ObjectId, ext = '') {
@param('realtime', Types.Boolean)
async get(domainId: string, tid: ObjectId, ext = '', realtime) {
if (!contest.canShowScoreboard.call(this, this.tdoc, true)) throw new ContestScoreboardHiddenError(tid);
if (realtime && !this.user.own(this.tdoc)) {
this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
}
if (ext) {
await this.exportScoreboard(domainId, tid, ext);
return;
}
const [, rows, udict, pdict] = await contest.getScoreboard.call(this, domainId, tid, false);
const config: ScoreboardConfig = { isExport: false };
if (!realtime && this.tdoc.lockAt && !this.tdoc.unlocked) {
config.lockAt = this.tdoc.lockAt;
}
const [, rows, udict, pdict] = await contest.getScoreboard.call(this, domainId, tid, config);
this.response.template = 'contest_scoreboard.html';
// eslint-disable-next-line @typescript-eslint/naming-convention
const page_name = this.tdoc.rule === 'homework'
@@ -334,7 +342,7 @@ export class ContestScoreboardHandler extends ContestDetailBaseHandler {
csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value?.toString().replace(/\n/g, ' ')).join(','))).join('\n')}`,
html: (rows, tdoc) => this.renderHTML('contest_scoreboard_download_html.html', { rows, tdoc }),
};
const [, rows] = await contest.getScoreboard.call(this, domainId, tid, true);
const [, rows] = await contest.getScoreboard.call(this, domainId, tid, { isExport: true, lockAt: this.tdoc.lockAt });
this.binary(await getContent[ext](rows, this.tdoc), `${this.tdoc.title}.${ext}`);
}
+3 -2
View File
@@ -7,7 +7,7 @@ import { Filter, ObjectId } from 'mongodb';
import { nanoid } from 'nanoid';
import { sortFiles, streamToBuffer } from '@hydrooj/utils/lib/utils';
import {
ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
FileLimitExceededError, HackFailedError, NoProblemError, NotFoundError,
PermissionError, ProblemAlreadyExistError, ProblemAlreadyUsedByContestError, ProblemConfigError,
ProblemIsReferencedError, ProblemNotAllowLanguageError, ProblemNotAllowPretestError, ProblemNotFoundError,
@@ -449,6 +449,7 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler {
@param('target', Types.String)
async postCopy(domainId: string, target: string) {
if (this.pdoc.reference) throw new BadRequestError('Cannot copy a referenced problem');
const t = `,${this.domain.share || ''},`;
if (t !== ',*,' && !t.includes(`,${target},`)) throw new PermissionError(target);
const ddoc = await domain.get(target);
@@ -1011,7 +1012,7 @@ export class ProblemPrefixListHandler extends Handler {
export async function apply(ctx) {
ctx.Route('problem_main', '/p', ProblemMainHandler, PERM.PERM_VIEW_PROBLEM);
ctx.Route('problem_random', '/problem/random', ProblemRandomHandler, PERM.PERM_VIEW_PROBLEM);
ctx.Route('problem_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_PROBLEM);
ctx.Route('problem_detail', '/p/:pid', ProblemDetailHandler);
ctx.Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM);
ctx.Route('problem_hack', '/p/:pid/hack/:rid', ProblemHackHandler, PERM.PERM_SUBMIT_PROBLEM);
ctx.Route('problem_edit', '/p/:pid/edit', ProblemEditHandler);
+8 -3
View File
@@ -510,6 +510,11 @@ export interface ContestStat extends Record<string, any> {
unrank?: boolean,
}
export interface ScoreboardConfig {
isExport: boolean;
lockAt?: Date;
}
export interface ContestRule<T = any> {
_originalRule?: Partial<ContestRule<T>>;
TEXT: string;
@@ -522,16 +527,16 @@ export interface ContestRule<T = any> {
showRecord: (tdoc: Tdoc<30>, now: Date) => boolean;
stat: (this: ContestRule<T>, tdoc: Tdoc<30>, journal: any[]) => ContestStat & T;
scoreboardHeader: (
this: ContestRule<T>, isExport: boolean, _: (s: string) => string,
this: ContestRule<T>, config: ScoreboardConfig, _: (s: string) => string,
tdoc: Tdoc<30>, pdict: ProblemDict,
) => Promise<ScoreboardRow>;
scoreboardRow: (
this: ContestRule<T>, isExport: boolean, _: (s: string) => string,
this: ContestRule<T>, config: ScoreboardConfig, _: (s: string) => string,
tdoc: Tdoc<30>, pdict: ProblemDict, udoc: BaseUser, rank: number, tsdoc: ContestStat & T,
meta?: any,
) => Promise<ScoreboardRow>;
scoreboard: (
this: ContestRule<T>, isExport: boolean, _: (s: string) => string,
this: ContestRule<T>, config: ScoreboardConfig, _: (s: string) => string,
tdoc: Tdoc<30>, pdict: ProblemDict, cursor: FindCursor<ContestStat & T>,
) => Promise<[board: ScoreboardRow[], udict: BaseUserDict]>;
ranked: (tdoc: Tdoc<30>, cursor: FindCursor<ContestStat & T>) => Promise<[number, ContestStat & T][]>;
+67 -68
View File
@@ -7,16 +7,14 @@ import {
} from '../error';
import {
BaseUserDict, ContestRule, ContestRules, ProblemDict,
ScoreboardNode, ScoreboardRow, SubtaskResult, Tdoc,
ScoreboardConfig, ScoreboardNode, ScoreboardRow, SubtaskResult, Tdoc,
} from '../interface';
import ranked from '../lib/rank';
import * as bus from '../service/bus';
import type { Handler } from '../service/server';
import { buildProjection } from '../utils';
import { PERM, STATUS, STATUS_SHORT_TEXTS } from './builtin';
import * as document from './document';
import problem from './problem';
import RecordModel from './record';
import user, { User } from './user';
interface AcmJournal {
@@ -59,23 +57,25 @@ const acm = buildContestRule({
stat(tdoc, journal: AcmJournal[]) {
const naccept = Counter<number>();
const npending = Counter<number>();
const effective: Record<number, AcmJournal> = {};
const display: Record<number, AcmJournal> = {};
const detail: Record<number, AcmDetail> = {};
let accept = 0;
let time = 0;
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const lockAt = isLocked(tdoc) ? tdoc.lockAt : null;
for (const j of journal) {
if (!this.submitAfterAccept && effective[j.pid]?.status === STATUS.STATUS_ACCEPTED) continue;
if (j.status === STATUS.STATUS_WAITING) {
if (!this.submitAfterAccept && display[j.pid]?.status === STATUS.STATUS_ACCEPTED) continue;
if (lockAt && j.rid.getTimestamp() > lockAt) {
npending[j.pid]++;
continue;
}
effective[j.pid] = j;
display[j.pid] = j;
if (![STATUS.STATUS_ACCEPTED, STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_FORMAT_ERROR].includes(j.status)) {
naccept[j.pid]++;
}
}
for (const pid in effective) {
const j = effective[pid];
for (const pid in display) {
const j = display[pid];
const real = Math.floor((j.rid.getTimestamp().getTime() - tdoc.beginAt.getTime()) / 1000);
const penalty = 20 * 60 * naccept[j.pid];
detail[pid] = {
@@ -86,14 +86,16 @@ const acm = buildContestRule({
accept++;
time += d.time;
}
return { accept, time, detail };
return {
accept, time, detail, display,
};
},
async scoreboardHeader(isExport, _, tdoc, pdict) {
async scoreboardHeader(config, _, tdoc, pdict) {
const columns: ScoreboardRow = [
{ type: 'rank', value: '#' },
{ type: 'user', value: _('User') },
];
if (isExport) {
if (config.isExport) {
columns.push({ type: 'email', value: _('Email') });
columns.push({ type: 'string', value: _('School') });
columns.push({ type: 'string', value: _('Name') });
@@ -103,7 +105,7 @@ const acm = buildContestRule({
for (let i = 1; i <= tdoc.pids.length; i++) {
const pid = tdoc.pids[i - 1];
pdict[pid].nAccept = pdict[pid].nSubmit = 0;
if (isExport) {
if (config.isExport) {
columns.push(
{
type: 'string',
@@ -124,13 +126,12 @@ const acm = buildContestRule({
}
return columns;
},
async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
const tsddict = tsdoc.detail || {};
async scoreboardRow(config, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
const row: ScoreboardRow = [
{ type: 'rank', value: rank.toString() },
{ type: 'user', value: udoc.uname, raw: tsdoc.uid },
];
if (isExport) {
if (config.isExport) {
row.push({ type: 'email', value: udoc.mail });
row.push({ type: 'string', value: udoc.school || '' });
row.push({ type: 'string', value: udoc.displayName || '' });
@@ -143,15 +144,17 @@ const acm = buildContestRule({
});
for (const s of tsdoc.journal || []) {
if (!pdict[s.pid]) continue;
if (config.lockAt && s.rid.getTimestamp() > config.lockAt) continue;
pdict[s.pid].nSubmit++;
if (s.status === STATUS.STATUS_ACCEPTED) pdict[s.pid].nAccept++;
}
const tsddict = (config.lockAt ? tsdoc.display : tsdoc.detail) || {};
for (const pid of tdoc.pids) {
const doc = tsddict[pid] || {} as Partial<AcmDetail>;
const accept = doc.status === STATUS.STATUS_ACCEPTED;
const colTime = accept ? formatSeconds(doc.real, false).toString() : '';
const colPenalty = doc.rid ? Math.ceil(doc.penalty / 60).toString() : '';
if (isExport) {
if (config.isExport) {
row.push(
{ type: 'string', value: colTime },
{ type: 'string', value: colPenalty },
@@ -175,7 +178,7 @@ const acm = buildContestRule({
}
return row;
},
async scoreboard(isExport, _, tdoc, pdict, cursor) {
async scoreboard(config, _, tdoc, pdict, cursor) {
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score && a.time === b.time);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getListForRender(tdoc.domainId, uids);
@@ -197,12 +200,12 @@ const acm = buildContestRule({
]).toArray() as any[];
for (const t of data) first[t._id] = t.first.getTimestamp().getTime();
const columns = await this.scoreboardHeader(isExport, _, tdoc, pdict);
const columns = await this.scoreboardHeader(config, _, tdoc, pdict);
const rows: ScoreboardRow[] = [
columns,
...await Promise.all(rankedTsdocs.map(
([rank, tsdoc]) => this.scoreboardRow(
isExport, _, tdoc, pdict, udict[tsdoc.uid], rank, tsdoc, { first },
config, _, tdoc, pdict, udict[tsdoc.uid], rank, tsdoc, { first },
),
)),
];
@@ -219,23 +222,36 @@ const oi = buildContestRule({
submitAfterAccept: true,
statusSort: { score: -1 },
stat(tdoc, journal) {
const npending = Counter();
const detail = {};
const display = {};
let score = 0;
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const lockAt = isLocked(tdoc) ? tdoc.lockAt : null;
for (const j of journal.filter((i) => tdoc.pids.includes(i.pid))) {
if (!detail[j.pid] || detail[j.pid].score < j.score || this.submitAfterAccept) detail[j.pid] = j;
if (!detail[j.pid] || detail[j.pid].score < j.score || this.submitAfterAccept) {
detail[j.pid] = j;
display[j.pid] ||= {};
if (lockAt && j.rid.getTimestamp() > lockAt) {
npending[j.pid]++;
display[j.pid].npending = npending[j.pid];
continue;
}
display[j.pid] = j;
}
}
for (const i in detail) score += detail[i].score;
return { score, detail };
for (const i in display) score += display[i].score || 0;
return { score, detail, display };
},
showScoreboard: (tdoc, now) => now > tdoc.endAt,
showSelfRecord: (tdoc, now) => now > tdoc.endAt,
showRecord: (tdoc, now) => now > tdoc.endAt,
async scoreboardHeader(isExport, _, tdoc, pdict) {
async scoreboardHeader(config, _, tdoc, pdict) {
const columns: ScoreboardNode[] = [
{ type: 'rank', value: '#' },
{ type: 'user', value: _('User') },
];
if (isExport) {
if (config.isExport) {
columns.push({ type: 'email', value: _('Email') });
columns.push({ type: 'string', value: _('School') });
columns.push({ type: 'string', value: _('Name') });
@@ -245,7 +261,7 @@ const oi = buildContestRule({
for (let i = 1; i <= tdoc.pids.length; i++) {
const pid = tdoc.pids[i - 1];
pdict[pid].nAccept = pdict[pid].nSubmit = 0;
if (isExport) {
if (config.isExport) {
columns.push({
type: 'string',
value: '#{0} {1}'.format(i, pdict[tdoc.pids[i - 1]].title),
@@ -260,13 +276,12 @@ const oi = buildContestRule({
}
return columns;
},
async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
const tsddict = tsdoc.detail || {};
async scoreboardRow(config, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
const row: ScoreboardNode[] = [
{ type: 'rank', value: rank.toString() },
{ type: 'user', value: udoc.uname, raw: tsdoc.uid },
];
if (isExport) {
if (config.isExport) {
row.push({ type: 'email', value: udoc.mail });
row.push({ type: 'string', value: udoc.school || '' });
row.push({ type: 'string', value: udoc.displayName || '' });
@@ -275,13 +290,15 @@ const oi = buildContestRule({
row.push({ type: 'total_score', value: tsdoc.score || 0 });
for (const s of tsdoc.journal || []) {
if (!pdict[s.pid]) continue;
if (config.lockAt && s.rid.getTimestamp() > config.lockAt) continue;
pdict[s.pid].nSubmit++;
if (s.status === STATUS.STATUS_ACCEPTED) pdict[s.pid].nAccept++;
}
const tsddict = (config.lockAt ? tsdoc.display : tsdoc.detail) || {};
for (const pid of tdoc.pids) {
const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`;
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const node: ScoreboardNode = (!isExport && isDone(tdoc)
const node: ScoreboardNode = (!config.isExport && !config.lockAt && isDone(tdoc)
&& meta?.psdict?.[index]?.rid
&& tsddict[pid]?.rid?.toHexString() !== meta?.psdict?.[index]?.rid?.toHexString())
? {
@@ -296,7 +313,8 @@ const oi = buildContestRule({
}],
} : {
type: 'record',
value: tsddict[pid]?.score ?? '-',
value: `${tsddict[pid]?.score ?? '-'}${tsddict[pid]?.npending
? `<span style="color:orange">+${tsddict[pid]?.npending}</span>` : ''}`,
raw: tsddict[pid]?.rid || null,
};
if (tsddict[pid]?.status === STATUS.STATUS_ACCEPTED && tsddict[pid]?.rid.getTimestamp().getTime() === meta?.first?.[pid]) {
@@ -306,7 +324,7 @@ const oi = buildContestRule({
}
return row;
},
async scoreboard(isExport, _, tdoc, pdict, cursor) {
async scoreboard(config, _, tdoc, pdict, cursor) {
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getListForRender(tdoc.domainId, uids);
@@ -332,12 +350,12 @@ const oi = buildContestRule({
}
}
}
const columns = await this.scoreboardHeader(isExport, _, tdoc, pdict);
const columns = await this.scoreboardHeader(config, _, tdoc, pdict);
const rows: ScoreboardRow[] = [
columns,
...await Promise.all(rankedTsdocs.map(
([rank, tsdoc]) => this.scoreboardRow(
isExport, _, tdoc, pdict, udict[tsdoc.uid], rank, tsdoc, { psdict, first },
config, _, tdoc, pdict, udict[tsdoc.uid], rank, tsdoc, { psdict, first },
),
)),
];
@@ -381,13 +399,13 @@ const strictioi = buildContestRule({
for (const i in detail) score += detail[i].score;
return { score, detail };
},
async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
async scoreboardRow(config, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
const tsddict = tsdoc.detail || {};
const row: ScoreboardNode[] = [
{ type: 'rank', value: rank.toString() },
{ type: 'user', value: udoc.uname, raw: tsdoc.uid },
];
if (isExport) {
if (config.isExport) {
row.push({ type: 'email', value: udoc.mail });
row.push({ type: 'string', value: udoc.school || '' });
row.push({ type: 'string', value: udoc.displayName || '' });
@@ -447,13 +465,13 @@ const ledo = buildContestRule({
score, originalScore, detail,
};
},
async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
async scoreboardRow(config, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
const tsddict = tsdoc.detail || {};
const row: ScoreboardRow = [
{ type: 'rank', value: rank.toString() },
{ type: 'user', value: udoc.uname, raw: tsdoc.uid },
];
if (isExport) {
if (config.isExport) {
row.push({ type: 'email', value: udoc.mail });
row.push({ type: 'string', value: udoc.school || '' });
row.push({ type: 'string', value: udoc.displayName || '' });
@@ -529,20 +547,20 @@ const homework = buildContestRule({
showScoreboard: () => true,
showSelfRecord: () => true,
showRecord: (tdoc, now) => now > tdoc.endAt,
async scoreboardHeader(isExport, _, tdoc, pdict) {
async scoreboardHeader(config, _, tdoc, pdict) {
const columns: ScoreboardNode[] = [
{ type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') },
{ type: 'total_score', value: _('Score') },
];
if (isExport) {
if (config.isExport) {
columns.push({ type: 'string', value: _('Original Score') });
}
columns.push({ type: 'time', value: _('Total Time') });
for (let i = 1; i <= tdoc.pids.length; i++) {
const pid = tdoc.pids[i - 1];
pdict[pid].nAccept = pdict[pid].nSubmit = 0;
if (isExport) {
if (config.isExport) {
columns.push(
{
type: 'string',
@@ -567,7 +585,7 @@ const homework = buildContestRule({
}
return columns;
},
async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc) {
async scoreboardRow(config, _, tdoc, pdict, udoc, rank, tsdoc) {
const tsddict = tsdoc.detail || {};
const row: ScoreboardRow = [
{ type: 'rank', value: rank.toString() },
@@ -581,7 +599,7 @@ const homework = buildContestRule({
value: tsdoc.penaltyScore || 0,
},
];
if (isExport) {
if (config.isExport) {
row.push({ type: 'string', value: tsdoc.score || 0 });
}
row.push({ type: 'time', value: formatSeconds(tsdoc.time || 0, false), raw: tsdoc.time });
@@ -596,7 +614,7 @@ const homework = buildContestRule({
const colOriginalScore = tsddict[pid]?.score ?? '';
const colTime = tsddict[pid]?.time || '';
const colTimeStr = colTime ? formatSeconds(colTime, false) : '';
if (isExport) {
if (config.isExport) {
row.push(
{ type: 'string', value: colScore },
{ type: 'string', value: colOriginalScore },
@@ -615,15 +633,15 @@ const homework = buildContestRule({
}
return row;
},
async scoreboard(isExport, _, tdoc, pdict, cursor) {
async scoreboard(config, _, tdoc, pdict, cursor) {
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getListForRender(tdoc.domainId, uids);
const columns = await this.scoreboardHeader(isExport, _, tdoc, pdict);
const columns = await this.scoreboardHeader(config, _, tdoc, pdict);
const rows: ScoreboardRow[] = [
columns,
...await Promise.all(rankedTsdocs.map(
([rank, tsdoc]) => this.scoreboardRow(isExport, _, tdoc, pdict, udict[tsdoc.uid], rank, tsdoc),
([rank, tsdoc]) => this.scoreboardRow(config, _, tdoc, pdict, udict[tsdoc.uid], rank, tsdoc),
)),
];
return [rows, udict];
@@ -695,12 +713,6 @@ async function _updateStatus(
tdoc: Tdoc<30>, uid: number, rid: ObjectId, pid: number, status: STATUS, score: number,
subtasks: Record<number, SubtaskResult>,
) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
if (isLocked(tdoc)) {
status = STATUS.STATUS_WAITING;
score = 0;
subtasks = {};
}
const tsdoc = await document.revPushStatus(tdoc.domainId, document.TYPE_CONTEST, tdoc.docId, uid, 'journal', {
rid, pid, status, score, subtasks,
}, 'rid');
@@ -827,19 +839,6 @@ export async function recalcStatus(domainId: string, tid: ObjectId) {
export async function unlockScoreboard(domainId: string, tid: ObjectId) {
const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid);
if (!tdoc.lockAt || tdoc.unlocked) return;
const rdocs = await RecordModel.getMulti(domainId, { contest: tid, _id: { $gte: Time.getObjectID(tdoc.lockAt) } })
.project(buildProjection(['_id', 'uid', 'pid', 'status', 'score'])).toArray();
for (const rdoc of rdocs) {
// TODO optimize
// eslint-disable-next-line
const tsdoc = await document.revPushStatus(domainId, document.TYPE_CONTEST, tid, rdoc.uid, 'journal', {
rid: rdoc._id, pid: rdoc.pid, status: rdoc.status, score: rdoc.score,
}, 'rid');
const journal = _getStatusJournal(tsdoc);
const stats = RULES[tdoc.rule].stat(tdoc, journal);
// eslint-disable-next-line
await document.revSetStatus(domainId, document.TYPE_CONTEST, tid, rdoc.uid, tsdoc.rev, { journal, ...stats });
}
await edit(domainId, tid, { unlocked: true });
}
@@ -868,14 +867,14 @@ export function canShowScoreboard(this: { user: User }, tdoc: Tdoc<30>, allowPer
}
export async function getScoreboard(
this: Handler, domainId: string, tid: ObjectId, isExport = false,
this: Handler, domainId: string, tid: ObjectId, config: ScoreboardConfig,
): Promise<[Tdoc<30>, ScoreboardRow[], BaseUserDict, ProblemDict]> {
const tdoc = await get(domainId, tid);
if (!canShowScoreboard.call(this, tdoc)) throw new ContestScoreboardHiddenError(tid);
const tsdocsCursor = getMultiStatus(domainId, { docId: tid }).sort(RULES[tdoc.rule].statusSort);
const pdict = await problem.getList(domainId, tdoc.pids, true, true, problem.PROJECTION_CONTEST_DETAIL);
const [rows, udict] = await RULES[tdoc.rule].scoreboard(
isExport, this.translate.bind(this),
config, this.translate.bind(this),
tdoc, pdict, tsdocsCursor,
);
await bus.parallel('contest/scoreboard', tdoc, rows, udict, pdict);
+3 -2
View File
@@ -490,7 +490,7 @@ Live...: 正在进行…
Loading editor...: 正在加载编辑器...
Loading file...: 正在加载文件内容...
Location: 位置
Lock scoreboard on last N minutes: 在赛事最后N分钟时锁定成绩表
Freeze scoreboard with N minutes remaining: 在赛事剩余N分钟时封榜
Login to Attend Contest: 登录后参加比赛
Login to Claim Homework: 登录后认领作业
Login to Create a Discussion: 登录后创建讨论
@@ -630,6 +630,7 @@ Please follow the instructions on your device to complete the verification.: 请
Please select at least one file to perform this operation.: 请选择至少一个文件来执行此操作。
Please select at least one role to perform this operation.: 请选择至少一个角色来进行操作。
Please select at least one user to perform this operation.: 请选择至少一个用户来进行操作。
Please wait until contest host unfreeze the scoreboard.: 请等待比赛主办方解除封榜。
Preference Settings: 偏好设置
Preparing Upload...: 准备上传...
Pretest Data: 自测数据
@@ -750,7 +751,6 @@ Saving file...: 正在保存文件...
score canceled: 成绩取消
Score: 分数
Scoreboard (Hidden): 成绩表 (隐藏)
Scoreboard locked at {0}: 比赛已于 {0} 封榜。
Scoreboard: 成绩表
Scoring method: 评分方式
Scripts: 脚本
@@ -850,6 +850,7 @@ The invitation code to enter to successfully join the domain. You can only use l
The invitation code you provided is invalid.: 您提供的邀请码无效。
The link will be expired at {0}: 链接将于 {0} 过期
The role to assign when user joining the domain.: 用户加入此域时分配的角色。
The scoreboard was frozen with {0} minutes remaining at {1} - submissions in the last {0} minutes of the contest are still shown as pending.: 比赛已于 {1} 在剩余 {0} 分钟时封榜 - 比赛最后 {0} 分钟的提交将显示为待定。
The uploaded file is too long.: 上传的文件太长。
The user is already a member of the domain.: 该用户已是域的成员。
The user is too lazy to leave something here...: 该用户太懒,这里啥也没写 (´・ω・`)
@@ -165,7 +165,7 @@
name:'lock',
placeholder:_('(leave blank if none)'),
value:(((tdoc.endAt - tdoc.lockAt)/60000)|int) if tdoc.lockAt else '',
help_text:_('Lock scoreboard on last N minutes'),
help_text:_('Freeze scoreboard with N minutes remaining'),
disabled:true,
row:false
}) }}
@@ -38,13 +38,13 @@
{% if model.contest.isLocked(tdoc) %}
<div class="section__body no-padding">
<blockquote class="note">
{{ _('Scoreboard locked at {0}').format(datetimeSpan(tdoc.lockAt))|safe }}
{{ _('The scoreboard was frozen with {0} minutes remaining at {1} - submissions in the last {0} minutes of the contest are still shown as pending.').format((tdoc.endAt - tdoc.lockAt) / 1000 / 60, datetimeSpan(tdoc.lockAt))|safe }}
</blockquote>
</div>
{% elif model.contest.isDone(tdoc) and tdoc.lockAt and not tdoc.unlocked %}
<div class="section__body no-padding">
<blockquote class="note">
{{ _('Please wait until contest host unlock the scoreboard.') }}
{{ _('Please wait until contest host unfreeze the scoreboard.') }}
</blockquote>
</div>
{% endif %}