diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 8d962fa92..ae36fcb2d 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -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}`); } diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 5d9b89210..a63aa75ad 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -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); diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 010e1cbe0..1b8340a90 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -510,6 +510,11 @@ export interface ContestStat extends Record { unrank?: boolean, } +export interface ScoreboardConfig { + isExport: boolean; + lockAt?: Date; +} + export interface ContestRule { _originalRule?: Partial>; TEXT: string; @@ -522,16 +527,16 @@ export interface ContestRule { showRecord: (tdoc: Tdoc<30>, now: Date) => boolean; stat: (this: ContestRule, tdoc: Tdoc<30>, journal: any[]) => ContestStat & T; scoreboardHeader: ( - this: ContestRule, isExport: boolean, _: (s: string) => string, + this: ContestRule, config: ScoreboardConfig, _: (s: string) => string, tdoc: Tdoc<30>, pdict: ProblemDict, ) => Promise; scoreboardRow: ( - this: ContestRule, isExport: boolean, _: (s: string) => string, + this: ContestRule, config: ScoreboardConfig, _: (s: string) => string, tdoc: Tdoc<30>, pdict: ProblemDict, udoc: BaseUser, rank: number, tsdoc: ContestStat & T, meta?: any, ) => Promise; scoreboard: ( - this: ContestRule, isExport: boolean, _: (s: string) => string, + this: ContestRule, config: ScoreboardConfig, _: (s: string) => string, tdoc: Tdoc<30>, pdict: ProblemDict, cursor: FindCursor, ) => Promise<[board: ScoreboardRow[], udict: BaseUserDict]>; ranked: (tdoc: Tdoc<30>, cursor: FindCursor) => Promise<[number, ContestStat & T][]>; diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 5a7119de7..f7218f4a2 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -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(); const npending = Counter(); - const effective: Record = {}; + const display: Record = {}; const detail: Record = {}; 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; 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 + ? `+${tsddict[pid]?.npending}` : ''}`, 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, ) { - // 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); diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index 555b1a633..e20576edf 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -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...: 该用户太懒,这里啥也没写 (´・ω・`) diff --git a/packages/ui-default/templates/contest_edit.html b/packages/ui-default/templates/contest_edit.html index 2a28b0315..84aa246ad 100644 --- a/packages/ui-default/templates/contest_edit.html +++ b/packages/ui-default/templates/contest_edit.html @@ -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 }) }} diff --git a/packages/ui-default/templates/contest_scoreboard.html b/packages/ui-default/templates/contest_scoreboard.html index ef83a2980..cb24abb74 100644 --- a/packages/ui-default/templates/contest_scoreboard.html +++ b/packages/ui-default/templates/contest_scoreboard.html @@ -38,13 +38,13 @@ {% if model.contest.isLocked(tdoc) %}
- {{ _('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 }}
{% elif model.contest.isDone(tdoc) and tdoc.lockAt and not tdoc.unlocked %}
- {{ _('Please wait until contest host unlock the scoreboard.') }} + {{ _('Please wait until contest host unfreeze the scoreboard.') }}
{% endif %}