diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 039478851..438da1dfc 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.14.4", + "version": "2.14.5", "bin": "bin/hydrooj.js", "main": "dist/loader.js", "typings": "dist/loader.d.ts", diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index e5ca1983e..277ea923f 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -143,6 +143,13 @@ class ContestScoreboardHandler extends ContestHandler { } } +class ContestScoreboardRawHandler extends ContestHandler { + @param('tid', Types.ObjectID) + async get(domainId: string, tid: ObjectID) { + this.response.body = await this.getRawStatus(domainId, tid); + } +} + class ContestScoreboardDownloadHandler extends ContestHandler { @param('tid', Types.ObjectID) @param('ext', Types.String) @@ -414,6 +421,7 @@ export async function apply() { Route('contest_boardcast', '/contest/:tid/boardcast', ContestBoardcastHandler); Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST); Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST); + Route('contest_scoreboard_raw', '/contest/:tid/scoreboard/raw', ContestScoreboardRawHandler, PERM.PERM_VIEW_CONTEST); Route('contest_scoreboard_download', '/contest/:tid/export/:ext', ContestScoreboardDownloadHandler, PERM.PERM_VIEW_CONTEST); Route('contest_detail_problem', '/contest/:tid/p/:pid', ContestProblemHandler, PERM.PERM_VIEW_CONTEST); Route('contest_detail_problem_submit', '/contest/:tid/p/:pid/submit', ContestProblemSubmitHandler, PERM.PERM_VIEW_CONTEST); diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 78d6334f9..895a9f141 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -282,6 +282,7 @@ export interface ScoreboardNode { value: string, raw?: any, } +export type ScoreboardRow = ScoreboardNode[] & { raw?: any }; export type PenaltyRules = Dictionary; @@ -423,9 +424,9 @@ export interface ContestRule { stat: (tdoc: Tdoc, journal: any[]) => ContestStat, scoreboard: ( isExport: boolean, _: (s: string) => string, - tdoc: Tdoc, rankedTsdocs: any[] | Generator, udict: Udict, pdict: Pdict - ) => ScoreboardNode[][], - rank: (tsdocs: any[]) => any[] | Generator, + tdoc: Tdoc, rankedTsdocs: any[], udict: Udict, pdict: Pdict + ) => ScoreboardRow[], + rank: (tsdocs: any[]) => any[], } export type ContestRules = Dictionary; diff --git a/packages/hydrooj/src/lib/rank.ts b/packages/hydrooj/src/lib/rank.ts index bf82a4847..9dfdac374 100644 --- a/packages/hydrooj/src/lib/rank.ts +++ b/packages/hydrooj/src/lib/rank.ts @@ -1,13 +1,15 @@ -function* ranked(diter: T[], equ = (a: T, b: T) => a === b): Generator<[number, T], void> { +function ranked(diter: T[], equ = (a: T, b: T) => a === b): [number, T][] { let last = null; let r = 0; let count = 0; + const results: [number, T][] = []; for (const doc of diter) { count++; if (count === 1 || !equ(last, doc)) r = count; last = doc; - yield [r, doc]; + results.push([r, doc]); } + return results; } global.Hydro.lib.rank = ranked; diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index b7d0b1e63..264d310e2 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -1,4 +1,5 @@ import { ObjectID, FilterQuery } from 'mongodb'; +import { filter, sortBy } from 'lodash'; import * as user from './user'; import * as problem from './problem'; import * as document from './document'; @@ -8,7 +9,7 @@ import { ContestNotAttendedError, ContestScoreboardHiddenError, } from '../error'; import { - ContestRule, ContestRules, ScoreboardNode, Tdoc, Udict, + ContestRule, ContestRules, ScoreboardNode, ScoreboardRow, Tdoc, Udict, } from '../interface'; import * as misc from '../lib/misc'; import ranked from '../lib/rank'; @@ -34,25 +35,24 @@ const acm: ContestRule = { if (!j.accept) naccept[j.pid] = (naccept[j.pid] || 0) + 1; } } - function _time(jdoc) { - const real = jdoc.rid.generationTime - Math.floor(tdoc.beginAt.getTime() / 1000); - const penalty = 20 * 60 * naccept[jdoc.pid]; - return real + penalty; - } for (const key in effective) { const j = effective[key]; + const real = j.rid.generationTime - Math.floor(tdoc.beginAt.getTime() / 1000); + const penalty = 20 * 60 * naccept[j.pid]; detail.push({ - ...j, naccept: naccept[j.pid], time: _time(j), + ...j, naccept: naccept[j.pid], time: real + penalty, real, penalty, }); } for (const d of detail) { - accept += d.accept; - if (d.accept) time += d.time; + if (d.accept) { + accept += d.accept; + time += d.time; + } } return { accept, time, detail }; }, scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { - const columns: ScoreboardNode[] = [ + const columns: ScoreboardRow = [ { type: 'rank', value: _('Rank') }, { type: 'user', value: _('User') }, { type: 'solved_problems', value: _('Solved Problems') }, @@ -88,13 +88,11 @@ const acm: ContestRule = { }); } } - const rows = [columns]; + const rows: ScoreboardRow[] = [columns]; for (const [rank, tsdoc] of rankedTsdocs) { const tsddict = {}; - if (tsdoc.detail) { - for (const item of tsdoc.detail) tsddict[item.pid] = item; - } - const row = []; + for (const item of tsdoc.detail || []) tsddict[item.pid] = item; + const row: ScoreboardRow = []; row.push( { type: 'string', value: rank }, { type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid }, @@ -134,6 +132,7 @@ const acm: ContestRule = { }); } } + row.raw = tsdoc; rows.push(row); } return rows; @@ -233,7 +232,7 @@ const homework: ContestRule = { if (exceedSeconds < 0) return score; let coefficient = 1; const keys = Object.keys(tdoc.penaltyRules) - .map((k) => parseFloat(k)).sort((a, b) => a - b); + .map(parseFloat).sort((a, b) => a - b); for (const i of keys) { if (i * 3600 <= exceedSeconds) coefficient = tdoc.penaltyRules[i]; else break; @@ -498,6 +497,17 @@ export const ContestHandlerMixin = (c) => class extends c { domainId: string, tid: ObjectID, isExport = false, docType: 30 | 60 = document.TYPE_CONTEST, ): Promise<[Tdoc, ScoreboardNode[][], Udict]> { + const { + tdoc, udict, pdict, rankedTsdocs, + } = await this.getRawStatus(domainId, tid, docType); + const rows = RULES[tdoc.rule].scoreboard( + isExport, this.translate.bind(this), + tdoc, rankedTsdocs, udict, pdict, + ); + return [tdoc, rows, udict]; + } + + async getRawStatus(domainId: string, tid: ObjectID, docType: 30 | 60 = document.TYPE_CONTEST) { const tdoc = await get(domainId, tid, docType); if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid); const tsdocs = await getMultiStatus(domainId, { docId: tid }, docType).sort(RULES[tdoc.rule].statusSort).toArray(); @@ -507,11 +517,9 @@ export const ContestHandlerMixin = (c) => class extends c { problem.getList(domainId, tdoc.pids, true), ]); const rankedTsdocs = RULES[tdoc.rule].rank(tsdocs); - const rows = RULES[tdoc.rule].scoreboard( - isExport, this.translate.bind(this), - tdoc, rankedTsdocs, udict, pdict, - ); - return [tdoc, rows, udict]; + return { + tdoc, tsdocs, udict, pdict, rankedTsdocs, + }; } // eslint-disable-next-line class-methods-use-this