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