mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +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,
|
||||
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}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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][]>;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in new issue
Block a user