diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 2b337f69d..0757cdea4 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "3.17.5", + "version": "3.17.6", "bin": "bin/hydrooj.js", "main": "src/loader", "module": "src/loader", diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 5fc21d469..2a51d19ef 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -119,7 +119,7 @@ export class ContestDetailHandler extends ContestDetailBaseHandler { this.response.template = 'contest_detail.html'; const udict = await user.getList(domainId, [this.tdoc.owner]); this.response.body = { - tdoc: this.tdoc, tsdoc: this.tsdoc, udict, + tdoc: this.tdoc, tsdoc: pick(this.tsdoc, ['attend', 'startAt']), udict, }; if (!this.request.json) { this.response.body.tdoc.content = this.response.body.tdoc.content diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index fb105402c..95a394a43 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -2,7 +2,7 @@ import AdmZip from 'adm-zip'; import { readFile, statSync } from 'fs-extra'; import { isBinaryFile } from 'isbinaryfile'; import { - escapeRegExp, flattenDeep, intersection, isSafeInteger, + escapeRegExp, flattenDeep, intersection, isSafeInteger, pick, } from 'lodash'; import { FilterQuery, ObjectID } from 'mongodb'; import { nanoid } from 'nanoid'; @@ -372,12 +372,12 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler { this.response.body = { pdoc: this.pdoc, udoc: this.udoc, - psdoc: this.psdoc, + psdoc: tid ? null : this.psdoc, title: this.pdoc.title, solutionCount: scnt, discussionCount: dcnt, tdoc: this.tdoc, - tsdoc: this.tsdoc, + tsdoc: pick(this.tsdoc, ['attend', 'startAt']), }; this.response.template = 'problem_detail.html'; this.UiContext.extraTitleContent = this.pdoc.title; @@ -429,8 +429,10 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler { if (this.psdoc?.rid) { this.response.body.rdoc = await record.get(this.args.domainId, this.psdoc.rid); } - this.response.body.ctdocs = await contest.getRelated(this.args.domainId, this.pdoc.docId); - this.response.body.htdocs = await contest.getRelated(this.args.domainId, this.pdoc.docId, 'homework'); + [this.response.body.ctdocs, this.response.body.htdocs] = await Promise.all([ + contest.getRelated(this.args.domainId, this.pdoc.docId), + contest.getRelated(this.args.domainId, this.pdoc.docId, 'homework'), + ]); } } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index dc2fde7f5..55f57244e 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -495,12 +495,16 @@ export interface ContestRule { TEXT: string; hidden?: boolean; check: (args: any) => any; - statusSort: any; + statusSort: Record; submitAfterAccept: boolean; showScoreboard: (tdoc: Tdoc<30>, now: Date) => boolean; showSelfRecord: (tdoc: Tdoc<30>, now: Date) => boolean; showRecord: (tdoc: Tdoc<30>, now: Date) => boolean; stat: (this: ContestRule, tdoc: Tdoc<30>, journal: any[], ignoreLock?: boolean) => ContestStat & T; + scoreboardHeader: ( + this: ContestRule, isExport: boolean, _: (s: string) => string, + tdoc: Tdoc<30>, pdict: ProblemDict, + ) => 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 8f77c6ef8..9a45d2a1f 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -1,3 +1,4 @@ +import { sumBy } from 'lodash'; import { FilterQuery, ObjectID } from 'mongodb'; import { Counter, formatSeconds, Time } from '@hydrooj/utils/lib/utils'; import { @@ -79,10 +80,7 @@ const acm = buildContestRule({ } return { accept, time, detail }; }, - 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); - const udict = await user.getList(tdoc.domainId, uids); + async scoreboardHeader(isExport, _, tdoc, pdict) { const columns: ScoreboardRow = [ { type: 'rank', value: '#' }, { type: 'user', value: _('User') }, @@ -110,7 +108,12 @@ const acm = buildContestRule({ }); } } - + return columns; + }, + 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); + const udict = await user.getList(tdoc.domainId, uids); // Find first accept const first = {}; const data = await document.collStatus.aggregate([ @@ -130,6 +133,7 @@ const acm = buildContestRule({ ]).toArray(); 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 = {}; @@ -197,10 +201,7 @@ const oi = buildContestRule({ showScoreboard: (tdoc, now) => now > tdoc.endAt, showSelfRecord: (tdoc, now) => now > tdoc.endAt, showRecord: (tdoc, now) => now > tdoc.endAt, - 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); + async scoreboardHeader(isExport, _, tdoc, pdict) { const columns: ScoreboardNode[] = [ { type: 'rank', value: '#' }, { type: 'user', value: _('User') }, @@ -221,6 +222,12 @@ const oi = buildContestRule({ }); } } + return columns; + }, + 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 psdict = {}; const first = {}; for (const pid of tdoc.pids) { @@ -243,6 +250,7 @@ const oi = buildContestRule({ } } } + const columns = await this.scoreboardHeader(isExport, _, tdoc, pdict); const rows: ScoreboardNode[][] = [columns]; for (const [rank, tsdoc] of rankedTsdocs) { const tsddict = {}; @@ -250,7 +258,7 @@ const oi = buildContestRule({ if (!tsddict[item.pid] || tsddict[item.pid].score < item.score || this.submitAfterAccept) tsddict[item.pid] = item; } const row: ScoreboardNode[] = [ - { type: 'string', value: rank.toString() }, + { 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 }); @@ -337,8 +345,8 @@ const homework = buildContestRule({ }); } return { - score: Math.sum(detail.map((d) => d.score)), - penaltyScore: Math.sum(detail.map((d) => d.penaltyScore)), + score: sumBy(detail, 'score'), + penaltyScore: sumBy(detail, 'penaltyScore'), time: Math.sum(detail.map((d) => d.time)), detail, }; @@ -346,10 +354,7 @@ const homework = buildContestRule({ showScoreboard: () => true, showSelfRecord: () => true, showRecord: () => true, - 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); + async scoreboardHeader(isExport, _, tdoc, pdict) { const columns: ScoreboardNode[] = [ { type: 'rank', value: _('Rank') }, { type: 'user', value: _('User') }, @@ -384,6 +389,13 @@ const homework = buildContestRule({ }); } } + return columns; + }, + 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 = {}; @@ -391,7 +403,7 @@ const homework = buildContestRule({ if (!tsddict[item.pid] || tsddict[item.pid].score <= item.score) tsddict[item.pid] = item; } const row: ScoreboardRow = [ - { type: 'string', value: rank }, + { type: 'rank', value: rank.toString() }, { type: 'user', value: udict[tsdoc.uid].uname, diff --git a/packages/hydrooj/src/pipelineUtils.ts b/packages/hydrooj/src/pipelineUtils.ts index dca71fbb7..eb78eacdd 100644 --- a/packages/hydrooj/src/pipelineUtils.ts +++ b/packages/hydrooj/src/pipelineUtils.ts @@ -2,8 +2,9 @@ import { FilterQuery } from 'mongodb'; import type { DomainDoc, ProblemStatusDoc, RecordDoc, - Udoc, + Tdoc, Udoc, } from './interface'; +import * as contest from './model/contest'; import * as document from './model/document'; import domain from './model/domain'; import problem, { Field, ProblemDoc } from './model/problem'; @@ -20,6 +21,15 @@ export async function iterateAllUser(cb: (udoc: Udoc, current?: number, total?: for (const i in udocs) await cb(udocs[i], +i, udocs.length); } +export async function iterateAllContest(cb: (tdoc: Tdoc) => Promise) { + await iterateAllDomain(async (ddoc) => { + const tdocs = await contest.getMulti(ddoc._id, {}).toArray(); + for (const tdoc of tdocs) { + await cb(tdoc); + } + }); +} + export async function iterateAllPsdoc(filter: FilterQuery, cb: (psdoc: ProblemStatusDoc) => Promise) { await iterateAllDomain(async ({ _id: domainId }) => { const cursor = document.getMultiStatus(domainId, document.TYPE_PROBLEM, filter); diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 8f19538b5..b773a6f47 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -831,6 +831,7 @@ const scripts: UpgradeScript[] = [ return true; }, async function _67_78() { + const _FRESH_INSTALL_IGNORE = 1; const rdocs = RecordModel.coll.find({ code: /^@@hydro_submission_file@@/ }); let rdoc; while (rdoc = await rdocs.next()) { // eslint-disable-line diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index c98758210..3f637e82f 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.40.5", + "version": "4.40.6", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/templates/contest_scoreboard.html b/packages/ui-default/templates/contest_scoreboard.html index 1024708fa..253b29b0a 100644 --- a/packages/ui-default/templates/contest_scoreboard.html +++ b/packages/ui-default/templates/contest_scoreboard.html @@ -30,7 +30,7 @@ {%- for column in rows[0] -%} - {% if column.type == 'problem' %} + {% if column.type == 'problem' and column.raw %} {{ column.value }}
{{ pdict[column.raw].nAccept }}/{{ pdict[column.raw].nSubmit }}