From 7b112817c6486eee4ff92f4ace8d96e18c80cf77 Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 14 Sep 2022 21:38:55 +0800 Subject: [PATCH] core: contestScoreboardRow method --- packages/hydrooj/src/interface.ts | 5 + packages/hydrooj/src/model/contest.ts | 281 ++++++++++++++------------ 2 files changed, 156 insertions(+), 130 deletions(-) diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 55f57244e..d0bd297df 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -505,6 +505,11 @@ export interface ContestRule { this: ContestRule, isExport: boolean, _: (s: string) => string, tdoc: Tdoc<30>, pdict: ProblemDict, ) => Promise; + scoreboardRow: ( + this: ContestRule, isExport: boolean, _: (s: string) => string, + tdoc: Tdoc<30>, pdict: ProblemDict, udoc: Udoc | User, rank: number, tsdoc: ContestStat & T, + meta?: any, + ) => Promise; scoreboard: ( this: ContestRule, isExport: boolean, _: (s: string) => string, tdoc: Tdoc<30>, pdict: ProblemDict, cursor: Cursor, page: number, diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 9a45d2a1f..057685c84 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -34,6 +34,8 @@ interface AcmDetail extends AcmJournal { function buildContestRule(def: ContestRule): ContestRule { const _originalRule = { scoreboard: def.scoreboard, stat: def.stat }; def.scoreboard = (def._originalRule?.scoreboard || def.scoreboard).bind(def); + def.scoreboardHeader = (def._originalRule?.scoreboardHeader || def.scoreboardHeader).bind(def); + def.scoreboardRow = (def._originalRule?.scoreboardRow || def.scoreboardRow).bind(def); def.stat = (def._originalRule?.stat || def.stat).bind(def); def._originalRule = _originalRule; return def; @@ -110,6 +112,48 @@ const acm = buildContestRule({ } return columns; }, + async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) { + const tsddict: Record = {}; + for (const item of tsdoc.detail || []) tsddict[item.pid] = item; + const row: ScoreboardRow = [ + { type: 'rank', value: rank.toString() }, + { type: 'user', value: udoc.uname, raw: tsdoc.uid }, + ]; + if (isExport) row.push({ type: 'email', value: udoc.mail }); + row.push({ + type: 'time', + value: `${tsdoc.accept || 0}\n${formatSeconds(tsdoc.time || 0.0, false)}`, + hover: formatSeconds(tsdoc.time || 0.0), + }); + 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) { + row.push( + { type: 'string', value: colTime }, + { type: 'string', value: colPenalty }, + ); + } else { + row.push({ + type: 'record', + score: accept ? 100 : 0, + value: accept + ? `${doc.naccept ? `+${doc.naccept}` : ''}\n${colTime}` + : doc.rid + ? `-${doc.naccept}` + : '', + hover: accept ? formatSeconds(doc.time) : '', + raw: doc.rid, + style: accept && doc.rid.generationTime === meta?.first?.[pid] + ? 'background-color: rgb(217, 240, 199);' + : undefined, + }); + } + } + return row; + }, async scoreboard(isExport, _, tdoc, pdict, cursor, page) { const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score && a.time === b.time, page); const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); @@ -134,49 +178,14 @@ const acm = buildContestRule({ for (const t of data) first[t._id] = t.first.generationTime; const columns = await this.scoreboardHeader(isExport, _, tdoc, pdict); - const rows: ScoreboardRow[] = [columns]; - for (const [rank, tsdoc] of rankedTsdocs) { - const tsddict: Record = {}; - for (const item of tsdoc.detail || []) tsddict[item.pid] = item; - const row: ScoreboardRow = [ - { type: 'rank', value: rank.toString() }, - { type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid }, - ]; - if (isExport) row.push({ type: 'email', value: udict[tsdoc.uid].mail }); - row.push({ - type: 'time', - value: `${tsdoc.accept || 0}\n${formatSeconds(tsdoc.time || 0.0, false)}`, - hover: formatSeconds(tsdoc.time || 0.0), - }); - 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) { - row.push( - { type: 'string', value: colTime }, - { type: 'string', value: colPenalty }, - ); - } else { - row.push({ - type: 'record', - score: accept ? 100 : 0, - value: accept - ? `${doc.naccept ? `+${doc.naccept}` : ''}\n${colTime}` - : doc.rid - ? `-${doc.naccept}` - : '', - hover: accept ? formatSeconds(doc.time) : '', - raw: doc.rid, - style: accept && doc.rid.generationTime === first[pid] - ? 'background-color: rgb(217, 240, 199);' - : undefined, - }); - } - } - rows.push(row); - } + const rows: ScoreboardRow[] = [ + columns, + ...await Promise.all(rankedTsdocs.map( + ([rank, tsdoc]) => this.scoreboardRow( + isExport, _, tdoc, pdict, udict[tsdoc.uid], rank, tsdoc, { first }, + ), + )), + ]; return [rows, udict, nPages]; }, async ranked(tdoc, cursor) { @@ -224,6 +233,45 @@ const oi = buildContestRule({ } return columns; }, + async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc, meta) { + const tsddict = {}; + for (const item of tsdoc.journal || []) { + if (!tsddict[item.pid] || tsddict[item.pid].score < item.score || this.submitAfterAccept) tsddict[item.pid] = item; + } + const row: ScoreboardNode[] = [ + { type: 'rank', value: rank.toString() }, + { type: 'user', value: udoc.uname, raw: tsdoc.uid }, + ]; + if (isExport) row.push({ type: 'email', value: udoc.mail }); + row.push({ type: 'total_score', value: tsdoc.score || 0 }); + 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) + && meta?.psdict?.[index]?.rid + && tsddict[pid]?.rid?.toHexString() !== meta?.psdict?.[index]?.rid?.toHexString()) + ? { + type: 'records', + value: '', + raw: [{ + value: tsddict[pid]?.score ?? '-', + raw: tsddict[pid]?.rid || null, + }, { + value: meta?.psdict?.[index]?.score ?? '-', + raw: meta?.psdict?.[index]?.rid ?? null, + }], + } : { + type: 'record', + value: tsddict[pid]?.score ?? '-', + raw: tsddict[pid]?.rid || null, + }; + if (tsddict[pid]?.status === STATUS.STATUS_ACCEPTED && tsddict[pid]?.rid.generationTime === meta?.first?.[pid]) { + node.style = 'background-color: rgb(217, 240, 199);'; + } + row.push(node); + } + return row; + }, async scoreboard(isExport, _, tdoc, pdict, cursor, page) { const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score, page); const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); @@ -251,46 +299,14 @@ const oi = buildContestRule({ } } const columns = await this.scoreboardHeader(isExport, _, tdoc, pdict); - const rows: ScoreboardNode[][] = [columns]; - for (const [rank, tsdoc] of rankedTsdocs) { - const tsddict = {}; - for (const item of tsdoc.journal || []) { - if (!tsddict[item.pid] || tsddict[item.pid].score < item.score || this.submitAfterAccept) tsddict[item.pid] = item; - } - const row: ScoreboardNode[] = [ - { type: 'rank', value: rank.toString() }, - { type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid }, - ]; - if (isExport) row.push({ type: 'email', value: udict[tsdoc.uid].mail }); - row.push({ type: 'total_score', value: tsdoc.score || 0 }); - 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) - && psdict[index]?.rid - && tsddict[pid]?.rid?.toHexString() !== psdict[index]?.rid?.toHexString()) - ? { - type: 'records', - value: '', - raw: [{ - value: tsddict[pid]?.score ?? '-', - raw: tsddict[pid]?.rid || null, - }, { - value: psdict[index]?.score ?? '-', - raw: psdict[index]?.rid ?? null, - }], - } : { - type: 'record', - value: tsddict[pid]?.score ?? '-', - raw: tsddict[pid]?.rid || null, - }; - if (tsddict[pid]?.status === STATUS.STATUS_ACCEPTED && tsddict[pid]?.rid.generationTime === first[pid]) { - node.style = 'background-color: rgb(217, 240, 199);'; - } - row.push(node); - } - rows.push(row); - } + const rows: ScoreboardRow[] = [ + columns, + ...await Promise.all(rankedTsdocs.map( + ([rank, tsdoc]) => this.scoreboardRow( + isExport, _, tdoc, pdict, udict[tsdoc.uid], rank, tsdoc, { psdict, first }, + ), + )), + ]; return [rows, udict, nPages]; }, async ranked(tdoc, cursor) { @@ -391,58 +407,63 @@ const homework = buildContestRule({ } return columns; }, + async scoreboardRow(isExport, _, tdoc, pdict, udoc, rank, tsdoc) { + const tsddict = {}; + for (const item of tsdoc.detail || []) { + if (!tsddict[item.pid] || tsddict[item.pid].score <= item.score) tsddict[item.pid] = item; + } + const row: ScoreboardRow = [ + { type: 'rank', value: rank.toString() }, + { + type: 'user', + value: udoc.uname, + raw: tsdoc.uid, + }, + { + type: 'string', + value: tsdoc.penaltyScore || 0, + }, + ]; + if (isExport) { + row.push({ type: 'string', value: tsdoc.score || 0 }); + } + row.push({ type: 'time', value: formatSeconds(tsdoc.time || 0, false), raw: tsdoc.time }); + for (const pid of tdoc.pids) { + const rid = tsddict[pid]?.rid; + const colScore = tsddict[pid]?.penaltyScore ?? ''; + const colOriginalScore = tsddict[pid]?.score ?? ''; + const colTime = tsddict[pid]?.time || ''; + const colTimeStr = colTime ? formatSeconds(colTime, false) : ''; + if (isExport) { + row.push( + { type: 'string', value: colScore }, + { type: 'string', value: colOriginalScore }, + { type: 'time', value: colTime }, + ); + } else { + row.push({ + type: 'record', + score: tsddict[pid]?.penaltyScore || 0, + value: colScore === colOriginalScore + ? '{0}\n{1}'.format(colScore, colTimeStr) + : '{0} / {1}\n{2}'.format(colScore, colOriginalScore, colTimeStr), + raw: rid, + }); + } + } + return row; + }, async scoreboard(isExport, _, tdoc, pdict, cursor, page) { const [rankedTsdocs, nPages] = await ranked(cursor, (a, b) => a.score === b.score, page); const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid); const udict = await user.getList(tdoc.domainId, uids); const columns = await this.scoreboardHeader(isExport, _, tdoc, pdict); - const rows: ScoreboardRow[] = [columns]; - for (const [rank, tsdoc] of rankedTsdocs) { - const tsddict = {}; - for (const item of tsdoc.detail || []) { - if (!tsddict[item.pid] || tsddict[item.pid].score <= item.score) tsddict[item.pid] = item; - } - const row: ScoreboardRow = [ - { type: 'rank', value: rank.toString() }, - { - type: 'user', - value: udict[tsdoc.uid].uname, - raw: tsdoc.uid, - }, - { - type: 'string', - value: tsdoc.penaltyScore || 0, - }, - ]; - if (isExport) { - row.push({ type: 'string', value: tsdoc.score || 0 }); - } - row.push({ type: 'time', value: formatSeconds(tsdoc.time || 0, false), raw: tsdoc.time }); - for (const pid of tdoc.pids) { - const rid = tsddict[pid]?.rid; - const colScore = tsddict[pid]?.penaltyScore ?? ''; - const colOriginalScore = tsddict[pid]?.score ?? ''; - const colTime = tsddict[pid]?.time || ''; - const colTimeStr = colTime ? formatSeconds(colTime, false) : ''; - if (isExport) { - row.push( - { type: 'string', value: colScore }, - { type: 'string', value: colOriginalScore }, - { type: 'time', value: colTime }, - ); - } else { - row.push({ - type: 'record', - score: tsddict[pid]?.penaltyScore || 0, - value: colScore === colOriginalScore - ? '{0}\n{1}'.format(colScore, colTimeStr) - : '{0} / {1}\n{2}'.format(colScore, colOriginalScore, colTimeStr), - raw: rid, - }); - } - } - rows.push(row); - } + const rows: ScoreboardRow[] = [ + columns, + ...await Promise.all(rankedTsdocs.map( + ([rank, tsdoc]) => this.scoreboardRow(isExport, _, tdoc, pdict, udict[tsdoc.uid], rank, tsdoc), + )), + ]; return [rows, udict, nPages]; }, async ranked(tdoc, cursor) {