From 0ece60b5dd08bf8e0e608d5845f32e3e250e2a1b Mon Sep 17 00:00:00 2001 From: Bhscer Date: Wed, 16 Jul 2025 22:45:56 +0800 Subject: [PATCH 01/27] a tmp commit --- packages/hydrooj/locales/zh.yaml | 3 + packages/hydrooj/src/handler/contest.ts | 76 ++++--- packages/hydrooj/src/handler/homework.ts | 15 +- packages/hydrooj/src/handler/problem.ts | 2 +- packages/hydrooj/src/handler/record.ts | 9 +- packages/hydrooj/src/interface.ts | 9 + packages/hydrooj/src/model/contest.ts | 170 +++++++++++---- packages/hydrooj/src/upgrade.ts | 25 ++- packages/onsite-toolkit/index.ts | 22 +- packages/scoreboard-xcpcio/index.ts | 12 +- packages/ui-default/backendlib/template.ts | 4 +- .../ContestProblemEditor.tsx | 206 ++++++++++++++++++ .../contestproblemeditor.page.styl | 11 + packages/ui-default/package.json | 2 + .../ui-default/pages/contest_balloon.page.tsx | 16 +- .../ui-default/pages/contest_edit.page.ts | 16 ++ .../templates/components/contest.html | 2 +- .../templates/components/problem.html | 4 +- .../ui-default/templates/contest_balloon.html | 2 +- .../ui-default/templates/contest_edit.html | 38 +++- .../ui-default/templates/contest_manage.html | 12 +- .../templates/contest_problemlist.html | 11 +- .../ui-default/templates/homework_detail.html | 7 +- .../ui-default/templates/homework_edit.html | 36 ++- .../templates/partials/contest_balloon.html | 2 +- .../templates/partials/contest_sidebar.html | 2 +- .../partials/contest_sidebar_management.html | 2 +- .../ui-default/templates/problem_detail.html | 11 +- .../ui-default/templates/record_main.html | 2 +- .../ui-default/templates/record_main_tr.html | 4 +- packages/utils/lib/common.ts | 2 + 31 files changed, 590 insertions(+), 145 deletions(-) create mode 100644 packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx create mode 100644 packages/ui-default/components/contestProblemEditor/contestproblemeditor.page.styl diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index 988e63af5e..1a9e3cbb47 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -211,6 +211,7 @@ Current password doesn't match.: 当前密码输入错误。 Current Password: 当前密码 Current Status: 当前状态 currently offline: 目前离线 +Custom Title: 自定义标题 Data of Problem {0} not found.: 题目 {0} 的数据缺失。 Data of problem {1} not found.: 题目 {1} 的数据未找到。 Data of record {0} not found.: 记录 {0} 的数据未找到。 @@ -435,6 +436,7 @@ Judged At: 评测时间 Judged By: 评测机 Judging Queue: 评测队列 Keep current expiration: 保持当前过期设置 +Label: 标号 Language: 语言 last active at: 最后活动于 last login at: 最后登录于 @@ -638,6 +640,7 @@ Quote: 引用 Rank: 排名 ranking: 排名 Ranking: 排名 +Raw Title: 原始标题 Read record codes after accept: 题目通过后读取记录的代码 Recalculates nSubmit and nAccept in problem status.: 重新计算每道题目的 AC 量和提交量 Recommended: 推荐 diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 31d3235a78..08eccf3580 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -5,7 +5,7 @@ import { escapeRegExp, pick } from 'lodash'; import moment from 'moment-timezone'; import { ObjectId } from 'mongodb'; import { - Counter, diffArray, getAlphabeticId, randomstring, sortFiles, Time, yaml, + Counter, diffArray, randomstring, sortFiles, Time, yaml, } from '@hydrooj/utils/lib/utils'; import { Context, Service } from '../context'; import { @@ -107,6 +107,10 @@ export class ContestDetailBaseHandler extends Handler { if (!tid || this.tdoc.rule === 'homework') return; if (this.request.json || !this.response.template) return; const pdoc = 'pdoc' in this ? (this as any).pdoc : {}; + const pid2idx = {}; + for (const [i, p] of this.tdoc.problems.entries()) { + pid2idx[p.pid] = i; + } this.response.body.overrideNav = [ { name: 'contest_main', @@ -132,7 +136,7 @@ export class ContestDetailBaseHandler extends Handler { }, { name: 'problem_detail', - displayName: `${getAlphabeticId(this.tdoc.pids.indexOf(pdoc.docId))}. ${pdoc.title}`, + displayName: `${this.tdoc.problems[pid2idx[pdoc.docId]]?.label}. ${this.tdoc.problems[pid2idx[pdoc.docId]]?.title ?? pdoc.title}`, args: { query: { tid }, pid: pdoc.docId, prefix: 'contest_detail_problem' }, checker: () => 'pdoc' in this, }, @@ -188,7 +192,7 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler { if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(domainId, tid); if (!this.tsdoc?.attend && !contest.isDone(this.tdoc)) throw new ContestNotAttendedError(domainId, tid); const [pdict, udict, tcdocs] = await Promise.all([ - problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST), + problem.getList(domainId, this.tdoc.problems.map((p) => p.pid), true, true, problem.PROJECTION_CONTEST_LIST), user.getList(domainId, [this.tdoc.owner, this.user._id]), contest.getMultiClarification(domainId, tid, this.user._id), ]); @@ -196,7 +200,7 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler { pdict, psdict: {}, udict, rdict: {}, tdoc: this.tdoc, tcdocs, }; this.response.template = 'contest_problemlist.html'; - this.response.body.showScore = Object.values(this.tdoc.score || {}).some((i) => i && i !== 100); + this.response.body.showScore = this.tdoc.problems.some((p) => p.score && p.score !== 100); if (!this.tsdoc) return; if (this.tsdoc.attend && !this.tsdoc.startAt && contest.isOngoing(this.tdoc)) { await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() }); @@ -209,7 +213,7 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler { this.response.body.canViewRecord = canViewRecord; const rids = psdocs.map((i) => i.rid); if (contest.isDone(this.tdoc) && canViewRecord) { - const correction = await problem.getListStatus(domainId, this.user._id, this.tdoc.pids); + const correction = await problem.getListStatus(domainId, this.user._id, this.tdoc.problems.map((p) => p.pid)); for (const pid in correction) { if (this.tsdoc.detail?.[pid]?.rid === correction[pid].rid) delete correction[pid]; } @@ -245,7 +249,7 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler { if (!this.user.own(this.tdoc)) { await message.send(1, this.tdoc.maintainer.concat(this.tdoc.owner), JSON.stringify({ message: 'Contest {0} has a new clarification about {1}, please go to contest management to reply.', - params: [this.tdoc.title, subject > 0 ? `#${this.tdoc.pids.indexOf(subject) + 1}` : 'the contest'], + params: [this.tdoc.title, subject > 0 ? `#${this.tdoc.problems.findIndex((p) => p.pid === subject) + 1}` : 'the contest'], url: this.url('contest_manage', { tid }), }), message.FLAG_I18N | message.FLAG_UNREAD); } @@ -283,7 +287,7 @@ export class ContestEditHandler extends Handler { rules, tdoc: this.tdoc, duration: tid ? -beginAt.diff(this.tdoc.endAt, 'hour', true) : 2, - pids: tid ? this.tdoc.pids.join(',') : '', + problems: tid ? this.tdoc.problems : [], beginAt, page_name: tid ? 'contest_edit' : 'contest_create', }; @@ -296,7 +300,7 @@ export class ContestEditHandler extends Handler { @param('title', Types.Title) @param('content', Types.Content) @param('rule', Types.Range(Object.keys(contest.RULES).filter((i) => !contest.RULES[i].hidden))) - @param('pids', Types.Content) + @param('problems', Types.Content) @param('rated', Types.Boolean) @param('code', Types.String, true) @param('autoHide', Types.Boolean) @@ -308,12 +312,13 @@ export class ContestEditHandler extends Handler { @param('langs', Types.CommaSeperatedArray, true) async postUpdate( domainId: string, tid: ObjectId, beginAtDate: string, beginAtTime: string, duration: number, - title: string, content: string, rule: string, _pids: string, rated = false, + title: string, content: string, rule: string, _problems: string, rated = false, _code = '', autoHide = false, assign: string[] = [], lock: number = null, contestDuration: number = null, maintainer: number[] = [], allowViewCode = false, langs: string[] = [], ) { if (autoHide) this.checkPerm(PERM.PERM_EDIT_PROBLEM); - const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i); + const { problems, score } = contest.resolveContestProblemJson(_problems); + const pids = problems.map((p) => p.pid); const beginAtMoment = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone); if (!beginAtMoment.isValid()) throw new ValidationError('beginAtDate', 'beginAtTime'); const endAt = beginAtMoment.clone().add(duration, 'hours').toDate(); @@ -324,15 +329,18 @@ export class ContestEditHandler extends Handler { await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id, true); if (tid) { await contest.edit(domainId, tid, { - title, content, rule, beginAt, endAt, pids, rated, duration: contestDuration, + title, content, rule, beginAt, endAt, pids, problems, score, rated, duration: contestDuration, }); if (this.tdoc.beginAt !== beginAt || this.tdoc.endAt !== endAt - || diffArray(this.tdoc.pids, pids) || this.tdoc.rule !== rule + || diffArray(this.tdoc.problems.map((i) => i.pid), pids) || this.tdoc.rule !== rule || lockAt !== this.tdoc.lockAt) { await contest.recalcStatus(domainId, this.tdoc.docId); } } else { - tid = await contest.add(domainId, title, content, this.user._id, rule, beginAt, endAt, pids, rated, { duration: contestDuration }); + tid = await contest.add( + domainId, title, content, this.user._id, rule, beginAt, endAt, pids, rated, + { duration: contestDuration, score, problems }, + ); } const task = { type: 'schedule', subType: 'contest', domainId, tid, @@ -438,7 +446,7 @@ export class ContestManagementHandler extends ContestManagementBaseHandler { tdoc: this.tdoc, tsdoc: this.tsdoc, owner_udoc: await user.getById(domainId, this.tdoc.owner), - pdict: await problem.getList(domainId, this.tdoc.pids, true, true, [...problem.PROJECTION_CONTEST_LIST, 'tag']), + pdict: await problem.getList(domainId, this.tdoc.problems.map((i) => i.pid), true, true, [...problem.PROJECTION_CONTEST_LIST, 'tag']), files: sortFiles(this.tdoc.files || []), udict: await user.getListForRender( domainId, tcdocs.map((i) => i.owner), @@ -516,10 +524,17 @@ export class ContestManagementHandler extends ContestManagementBaseHandler { @param('pid', Types.PositiveInt) @param('score', Types.PositiveInt) async postSetScore(domainId: string, pid: number, score: number) { - if (!this.tdoc.pids.includes(pid)) throw new ValidationError('pid'); - this.tdoc.score ||= {}; - this.tdoc.score[pid] = score; - await contest.edit(domainId, this.tdoc.docId, { score: this.tdoc.score }); + const idx = this.tdoc.problems.findIndex((i) => i.pid === pid); + if (idx === -1) throw new ValidationError('pid'); + this.tdoc.problems[idx].score = score; + // TODO: remove `score` field later + this.tdoc.score = this.tdoc.problems.reduce( + (acc, cur) => { + if (cur.score) acc[cur.pid] = cur.score; + return acc; + }, {}, + ); + await contest.edit(domainId, this.tdoc.docId, { score: this.tdoc.score, problems: this.tdoc.problems }); await contest.recalcStatus(domainId, this.tdoc.docId); this.back(); } @@ -593,7 +608,7 @@ export class ContestBalloonHandler extends ContestManagementBaseHandler { tdoc: this.tdoc, tsdoc: this.tsdoc, owner_udoc: await user.getById(domainId, this.tdoc.owner), - pdict: await problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST), + pdict: await problem.getList(domainId, this.tdoc.problems.map((i) => i.pid), true, true, problem.PROJECTION_CONTEST_LIST), bdocs, udict: await user.getListForRender(domainId, uids, this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) ? ['displayName'] : []), }; @@ -607,11 +622,13 @@ export class ContestBalloonHandler extends ContestManagementBaseHandler { const config = yaml.load(color); if (typeof config !== 'object') throw new ValidationError('color'); const balloon = {}; - for (const pid of this.tdoc.pids) { + for (let i = 0; i < this.tdoc.problems.length; i++) { + const pid = this.tdoc.problems[i].pid; if (!config[pid]) throw new ValidationError('color'); balloon[pid] = config[pid.toString()]; + this.tdoc.problems[i].balloon = config[pid.toString()]; } - await contest.edit(domainId, tid, { balloon }); + await contest.edit(domainId, tid, { balloon, problems: this.tdoc.problems }); this.back(); } @@ -745,7 +762,7 @@ export async function apply(ctx: Context) { const tasks = []; for (const op of doc.operation) { if (op === 'unhide') { - for (const pid of tdoc.pids) { + for (const pid of tdoc.problems.map((i) => i.pid)) { tasks.push(problem.edit(doc.domainId, pid, { hidden: false })); } } @@ -785,14 +802,13 @@ export async function apply(ctx: Context) { this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); } const [pdict, teams] = await Promise.all([ - problem.getList(tdoc.domainId, tdoc.pids, true, false, problem.PROJECTION_LIST, true), + problem.getList(tdoc.domainId, tdoc.problems.map((i) => i.pid), true, false, problem.PROJECTION_LIST, true), contest.getMultiStatus(tdoc.domainId, { docId: tdoc._id }).toArray(), ]); const udict = await user.getList(tdoc.domainId, teams.map((i) => i.uid)); const teamIds: Record = {}; for (let i = 1; i <= teams.length; i++) teamIds[teams[i - 1].uid] = i; const time = (t: ObjectId) => Math.floor((t.getTimestamp().getTime() - tdoc.beginAt.getTime()) / Time.second); - const pid = (i: number) => getAlphabeticId(i); const escape = (i: string) => i.replace(/[",]/g, ''); const unknownSchool = this.translate('Unknown School'); const statusMap = { @@ -804,10 +820,14 @@ export async function apply(ctx: Context) { }; const submissions = teams.flatMap((i, idx) => { if (!i.journal) return []; - const journal = i.journal.filter((s) => tdoc.pids.includes(s.pid)); + const pid2idx = {}; + for (let j = 0; j < tdoc.problems.length; j++) { + pid2idx[tdoc.problems[j].pid] = j; + } + const journal = i.journal.filter((s) => pid2idx[s.pid] !== undefined); const c = Counter(); return journal.map((s) => { - const id = pid(tdoc.pids.indexOf(s.pid)); + const id = tdoc.problems[pid2idx[s.pid]].label; c[id]++; return `@s ${idx + 1},${id},${c[id]},${time(s.rid)},${statusMap[s.status] || 'RJ'}`; }); @@ -815,11 +835,11 @@ export async function apply(ctx: Context) { const res = [ `@contest "${escape(tdoc.title)}"`, `@contlen ${Math.floor((tdoc.endAt.getTime() - tdoc.beginAt.getTime()) / Time.minute)}`, - `@problems ${tdoc.pids.length}`, + `@problems ${tdoc.problems.length}`, `@teams ${tdoc.attend}`, `@submissions ${submissions.length}`, ].concat( - tdoc.pids.map((i, idx) => `@p ${pid(idx)},${escape(pdict[i]?.title || 'Unknown Problem')},20,0`), + tdoc.problems.map((p) => `@p ${p.label},${escape(p.title || pdict[p.pid]?.title || 'Unknown Problem')},20,0`), teams.map((i, idx) => { const showName = this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) && udict[i.uid].displayName ? udict[i.uid].displayName : udict[i.uid].uname; diff --git a/packages/hydrooj/src/handler/homework.ts b/packages/hydrooj/src/handler/homework.ts index 033708717b..69258f2d62 100644 --- a/packages/hydrooj/src/handler/homework.ts +++ b/packages/hydrooj/src/handler/homework.ts @@ -173,7 +173,7 @@ class HomeworkEditHandler extends Handler { timePenaltyText: penaltySince.format('H:mm'), extensionDays, penaltyRules: tid ? yaml.dump(tdoc.penaltyRules) : null, - pids: tid ? tdoc.pids.join(',') : '', + problems: tid ? tdoc.problems : [], page_name: tid ? 'homework_edit' : 'homework_create', }; } @@ -187,7 +187,7 @@ class HomeworkEditHandler extends Handler { @param('penaltyRules', Types.Content, validatePenaltyRules, convertPenaltyRules) @param('title', Types.Title) @param('content', Types.Content) - @param('pids', Types.Content) + @param('problems', Types.Content) @param('rated', Types.Boolean) @param('maintainer', Types.NumericArray, true) @param('assign', Types.CommaSeperatedArray, true) @@ -195,10 +195,11 @@ class HomeworkEditHandler extends Handler { async postUpdate( domainId: string, tid: ObjectId, beginAtDate: string, beginAtTime: string, penaltySinceDate: string, penaltySinceTime: string, extensionDays: number, - penaltyRules: PenaltyRules, title: string, content: string, _pids: string, rated = false, + penaltyRules: PenaltyRules, title: string, content: string, _problems: string, rated = false, maintainer: number[] = [], assign: string[] = [], langs: string[] = [], ) { - const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i); + const { problems, score } = contest.resolveContestProblemJson(_problems); + const pids = problems.map((p) => p.pid); const tdoc = tid ? await contest.get(domainId, tid) : null; if (!tid) this.checkPerm(PERM.PERM_CREATE_HOMEWORK); else if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_HOMEWORK); @@ -214,7 +215,9 @@ class HomeworkEditHandler extends Handler { if (!tid) { tid = await contest.add(domainId, title, content, this.user._id, 'homework', beginAt.toDate(), endAt.toDate(), pids, rated, - { penaltySince: penaltySince.toDate(), penaltyRules, assign }); + { + penaltySince: penaltySince.toDate(), penaltyRules, assign, problems, score, + }); } else { await contest.edit(domainId, tid, { title, @@ -222,6 +225,8 @@ class HomeworkEditHandler extends Handler { beginAt: beginAt.toDate(), endAt: endAt.toDate(), pids, + problems, + score, penaltySince: penaltySince.toDate(), penaltyRules, rated, diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 463cf26068..5b2d71eb5e 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -278,7 +278,7 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler { this.pdoc = await problem.get(domainId, pid); if (!this.pdoc) throw new ProblemNotFoundError(domainId, pid); if (tid) { - if (!this.tdoc?.pids?.includes(this.pdoc.docId)) throw new ContestNotFoundError(domainId, tid); + if (this.tdoc?.problems?.findIndex((p) => p.pid === this.pdoc.docId) === -1) throw new ContestNotFoundError(domainId, tid); if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(tid); if (!contest.isDone(this.tdoc, this.tsdoc) && (!this.tsdoc?.attend || !this.tsdoc.startAt)) throw new ContestNotAttendedError(tid); // Delete problem-related info in contest mode diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index aea23f4328..ec4fc1e64c 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -68,9 +68,12 @@ class RecordListHandler extends ContestDetailBaseHandler { notification.push({ name, args: { type: 'note' }, checker: () => true }); } } + // in order to make contest's submissionList can show label like A + const realPid = pid; if (pid) { - if (typeof pid === 'string' && tdoc && /^[A-Z]$/.test(pid)) { - pid = tdoc.pids[parseInt(pid, 36) - 10]; + if (typeof pid === 'string' && tdoc) { + const result = tdoc.problems.find((i) => i.label === pid); + if (result) pid = result.pid; } const pdoc = await problem.get(domainId, pid); if (pdoc) q.pid = pdoc.docId; @@ -114,7 +117,7 @@ class RecordListHandler extends ContestDetailBaseHandler { udict, all, allDomain, - filterPid: pid, + filterPid: realPid, filterTid: tid, filterUidOrName: uidOrName, filterLang: lang, diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 04ab4e8059..7ed77dfaef 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -247,6 +247,14 @@ export interface TrainingNode { pids: number[], } +export interface ContestProblem { + pid: number; + label: string; + title?: string; + score?: number; + balloon?: { color: string, name: string }; +} + export interface Tdoc extends Document { docId: ObjectId; docType: document['TYPE_CONTEST']; @@ -257,6 +265,7 @@ export interface Tdoc extends Document { content: string; rule: string; pids: number[]; + problems: ContestProblem[]; rated?: boolean; _code?: string; assign?: string[]; diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index b86f95b660..0461b1a9c1 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -8,7 +8,7 @@ import { ContestScoreboardHiddenError, ValidationError, } from '../error'; import { - BaseUserDict, ContestRule, ContestRules, ProblemDict, RecordDoc, + BaseUserDict, ContestProblem, ContestRule, ContestRules, ProblemDict, RecordDoc, ScoreboardConfig, ScoreboardNode, ScoreboardRow, SubtaskResult, Tdoc, } from '../interface'; import bus from '../service/bus'; @@ -103,7 +103,7 @@ const acm = buildContestRule({ let time = 0; const lockAt = isLocked(tdoc) ? tdoc.lockAt : null; for (const j of journal) { - if (!tdoc.pids.includes(j.pid)) continue; + if (!tdoc.problems.find((p) => p.pid === j.pid)) continue; if (!this.submitAfterAccept && display[j.pid]?.status === STATUS.STATUS_ACCEPTED) continue; if (![STATUS.STATUS_ACCEPTED, STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_FORMAT_ERROR, STATUS.STATUS_CANCELED].includes(j.status)) { naccept[j.pid]++; @@ -143,8 +143,8 @@ const acm = buildContestRule({ columns.push({ type: 'string', value: _('Student ID') }); } columns.push({ type: 'solved', value: `${_('Solved')}\n${_('Total Time')}` }); - for (let i = 1; i <= tdoc.pids.length; i++) { - const pid = tdoc.pids[i - 1]; + for (let i = 1; i <= tdoc.problems.length; i++) { + const pid = tdoc.problems[i - 1].pid; pdict[pid].nAccept = pdict[pid].nSubmit = 0; if (config.isExport) { columns.push( @@ -194,7 +194,8 @@ const acm = buildContestRule({ } } const tsddict = (config.lockAt ? tsdoc.display : tsdoc.detail) || {}; - for (const pid of tdoc.pids) { + for (const p of tdoc.problems) { + const pid = p.pid; const doc = tsddict[pid] || {} as Partial; const accept = doc.status === STATUS.STATUS_ACCEPTED; const colTime = accept ? formatSeconds(doc.real, false).toString() : ''; @@ -283,7 +284,7 @@ const oi = buildContestRule({ let score = 0; const lockAt = isLocked(tdoc) ? tdoc.lockAt : null; - for (const j of journal.filter((i) => tdoc.pids.includes(i.pid))) { + for (const j of journal.filter((i) => tdoc.problems.find((p) => p.pid === i.pid))) { if (lockAt && j.rid.getTimestamp() > lockAt) { npending[j.pid]++; display[j.pid] ||= {}; @@ -296,7 +297,7 @@ const oi = buildContestRule({ } } for (const i in display) { - score += ((tdoc.score?.[i] || 100) * (display[i].score || 0)) / 100; + score += ((tdoc.problems.find((p) => p.pid.toString() === i)?.score || 100) * (display[i].score || 0)) / 100; } return { score, detail, display }; }, @@ -315,19 +316,20 @@ const oi = buildContestRule({ columns.push({ type: 'string', value: _('Student ID') }); } columns.push({ type: 'total_score', value: _('Total Score') }); - for (let i = 1; i <= tdoc.pids.length; i++) { - const pid = tdoc.pids[i - 1]; + for (let i = 1; i <= tdoc.problems.length; i++) { + const cp = tdoc.problems[i - 1]; + const pid = cp.pid; pdict[pid].nAccept = pdict[pid].nSubmit = 0; if (config.isExport) { columns.push({ type: 'string', - value: '#{0} {1}'.format(i, pdict[tdoc.pids[i - 1]].title), + value: '#{0} {1}'.format(i, cp.title || pdict[pid].title), }); } else { columns.push({ type: 'problem', - value: getAlphabeticId(i - 1), - raw: tdoc.pids[i - 1], + value: cp.label, + raw: cp, }); } } @@ -340,7 +342,7 @@ const oi = buildContestRule({ ]; const displayScore = (pid: number, score?: number) => { if (typeof score !== 'number') return '-'; - return score * ((tdoc.score?.[pid] || 100) / 100); + return score * ((tdoc.problems.find((p) => p.pid === pid)?.score || 100) / 100); }; if (config.isExport && config.showDisplayName) { row.push({ type: 'email', value: udoc.mail }); @@ -360,7 +362,8 @@ const oi = buildContestRule({ } } const tsddict = ((config.lockAt && isLocked(tdoc, new Date())) ? tsdoc.display : tsdoc.detail) || {}; - for (const pid of tdoc.pids) { + for (const cp of tdoc.problems) { + const pid = cp.pid; const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`; const node: ScoreboardNode = (!config.isExport && !config.lockAt && isDone(tdoc) @@ -411,7 +414,7 @@ const oi = buildContestRule({ if (isDone(tdoc)) { const psdocs = await Promise.all( - tdoc.pids.map((pid) => problem.getMultiStatus(tdoc.domainId, { docId: pid, uid: { $in: uids } }).toArray()), + tdoc.problems.map(({ pid }) => problem.getMultiStatus(tdoc.domainId, { docId: pid, uid: { $in: uids } }).toArray()), ); for (const tpsdoc of psdocs) { for (const psdoc of tpsdoc) { @@ -469,7 +472,7 @@ const strictioi = buildContestRule({ const detail = {}; let score = 0; const subtasks: Record> = {}; - for (const j of journal.filter((i) => tdoc.pids.includes(i.pid))) { + for (const j of journal.filter((i) => tdoc.problems.find((p) => p.pid === i.pid))) { subtasks[j.pid] ||= {}; for (const i in j.subtasks) { if (!subtasks[j.pid][i] || subtasks[j.pid][i].score < j.subtasks[i].score) subtasks[j.pid][i] = j.subtasks[i]; @@ -478,7 +481,7 @@ const strictioi = buildContestRule({ j.status = Math.max(...Object.values(subtasks[j.pid]).map((i) => i.status)); if (!detail[j.pid] || detail[j.pid].score < j.score) detail[j.pid] = { ...j, subtasks: subtasks[j.pid] }; } - for (const i in detail) score += ((tdoc.score?.[i] || 100) * (detail[i].score || 0)) / 100; + for (const i in detail) score += ((tdoc.problems.find((p) => p.pid.toString() === i)?.score || 100) * (detail[i].score || 0)) / 100; return { score, detail }; }, async scoreboardRow(config, _, tdoc, pdict, udoc, rank, tsdoc, meta) { @@ -503,7 +506,8 @@ const strictioi = buildContestRule({ accepted[s.pid] = true; } } - for (const pid of tdoc.pids) { + for (const cp of tdoc.problems) { + const pid = cp.pid; const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`; const n: ScoreboardNode = (!config.isExport && !config.lockAt && isDone(tdoc) && meta?.psdict?.[index]?.rid @@ -513,17 +517,19 @@ const strictioi = buildContestRule({ type: 'records', value: '', raw: [{ - value: ((tsddict[pid]?.score || 0) * ((tdoc.score?.[pid] || 100) / 100)).toString() || '', + value: ((tsddict[pid]?.score || 0) * ((tdoc.problems.find((p) => p.pid === pid)?.score || 100) / 100)).toString() || '', raw: tsddict[pid]?.rid || null, score: tsddict[pid]?.score, }, { - value: ((meta?.psdict?.[index]?.score || 0) * ((tdoc.score?.[pid] || 100) / 100)).toString() || '', + value: ( + (meta?.psdict?.[index]?.score || 0) * ((tdoc.problems.find((p) => p.pid === pid)?.score || 100) / 100) + ).toString() || '', raw: meta?.psdict?.[index]?.rid ?? null, score: meta?.psdict?.[index]?.score, }], } : { type: 'record', - value: ((tsddict[pid]?.score || 0) * ((tdoc.score?.[pid] || 100) / 100)).toString() || '', + value: ((tsddict[pid]?.score || 0) * ((tdoc.problems.find((p) => p.pid === pid)?.score || 100) / 100)).toString() || '', raw: tsddict[pid]?.rid, score: tsddict[pid]?.score, }; @@ -548,7 +554,7 @@ const ledo = buildContestRule({ stat(tdoc, journal) { const ntry = Counter(); const detail = {}; - for (const j of journal.filter((i) => tdoc.pids.includes(i.pid))) { + for (const j of journal.filter((i) => tdoc.problems.find((p) => p.pid === i.pid))) { const vaild = ![STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_FORMAT_ERROR].includes(j.status); if (vaild) ntry[j.pid]++; const penaltyScore = vaild ? Math.round(Math.max(0.7, 0.95 ** (ntry[j.pid] - 1)) * j.score) : 0; @@ -562,9 +568,10 @@ const ledo = buildContestRule({ } let score = 0; let originalScore = 0; - for (const pid of tdoc.pids) { + for (const cp of tdoc.problems) { + const pid = cp.pid; if (!detail[pid]) continue; - const rate = (tdoc.score?.[pid] || 100) / 100; + const rate = (tdoc.problems.find((p) => p.pid === pid)?.score || 100) / 100; score += detail[pid].penaltyScore * rate; originalScore += detail[pid].score * rate; } @@ -598,10 +605,11 @@ const ledo = buildContestRule({ accepted[s.pid] = true; } } - for (const pid of tdoc.pids) { + for (const cp of tdoc.problems) { + const pid = cp.pid; row.push({ type: 'record', - value: ((tsddict[pid]?.penaltyScore || 0) * ((tdoc.score?.[pid] || 100) / 100)).toString(), + value: ((tsddict[pid]?.penaltyScore || 0) * ((tdoc.problems.find((p) => p.pid === pid)?.score || 100) / 100)).toString(), hover: tsddict[pid]?.ntry ? `-${tsddict[pid].ntry} (${Math.round(Math.max(0.7, 0.95 ** tsddict[pid].ntry) * 100)}%)` : '', raw: tsddict[pid]?.rid, score: tsddict[pid]?.score, @@ -627,7 +635,7 @@ const homework = buildContestRule({ stat: (tdoc, journal) => { const effective = {}; for (const j of journal) { - if (tdoc.pids.includes(j.pid)) effective[j.pid] = j; + if (tdoc.problems.find((p) => p.pid === j.pid)) effective[j.pid] = j; } function time(jdoc) { const real = (jdoc.rid.getTimestamp().getTime() - tdoc.beginAt.getTime()) / 1000; @@ -635,7 +643,7 @@ const homework = buildContestRule({ } function penaltyScore(jdoc) { - const rate = (tdoc.score?.[jdoc.pid] || 100) / 100; + const rate = (tdoc.problems.find((p) => p.pid.toString() === jdoc.pid)?.score || 100) / 100; const exceedSeconds = Math.floor( (jdoc.rid.getTimestamp().getTime() - tdoc.penaltySince.getTime()) / 1000, ); @@ -680,14 +688,15 @@ const homework = buildContestRule({ 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]; + for (let i = 1; i <= tdoc.problems.length; i++) { + const cp = tdoc.problems[i - 1]; + const pid = cp.pid; pdict[pid].nAccept = pdict[pid].nSubmit = 0; if (config.isExport) { columns.push( { type: 'string', - value: '#{0} {1}'.format(i, pdict[pid].title), + value: '#{0} {1}'.format(i, cp.title || pdict[pid].title), }, { type: 'string', @@ -701,7 +710,7 @@ const homework = buildContestRule({ } else { columns.push({ type: 'problem', - value: getAlphabeticId(i - 1), + value: cp.label, raw: pid, }); } @@ -738,7 +747,8 @@ const homework = buildContestRule({ accepted[s.pid] = true; } } - for (const pid of tdoc.pids) { + for (const cp of tdoc.problems) { + const pid = cp.pid; const rid = tsddict[pid]?.rid; const colScore = tsddict[pid]?.penaltyScore ?? ''; const colOriginalScore = tsddict[pid]?.score ?? ''; @@ -793,18 +803,27 @@ function _getStatusJournal(tsdoc) { export async function add( domainId: string, title: string, content: string, owner: number, - rule: string, beginAt = new Date(), endAt = new Date(), pids: number[] = [], - rated = false, data: Partial = {}, + rule: string, beginAt = new Date(), endAt = new Date(), + pids: number[] = [], rated = false, data: Partial = {}, ) { if (!RULES[rule]) throw new ValidationError('rule'); if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt'); + // TODO: this is the best way to support old plugins, but need remove one day + let problems = data?.problems || []; + if (problems.length === 0 && pids.length > 0) { + problems = pids.map((pid, idx) => ({ + pid, + label: getAlphabeticId(idx), + ...(data?.score && data.score[pid] ? { score: data.score[pid] } : {}), + })); + } Object.assign(data, { - content, owner, title, rule, beginAt, endAt, pids, attend: 0, + content, owner, title, rule, beginAt, endAt, pids, problems, attend: 0, }); RULES[rule].check(data); await bus.parallel('contest/before-add', data); const docId = await document.add(domainId, content, owner, document.TYPE_CONTEST, null, null, null, { - assign: [], ...data, title, rule, beginAt, endAt, pids, attend: 0, rated, + assign: [], ...data, title, rule, beginAt, endAt, pids, problems, attend: 0, rated, }); await bus.parallel('contest/add', data, docId); return docId; @@ -815,6 +834,47 @@ export async function edit(domainId: string, tid: ObjectId, $set: Partial) const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); if (!tdoc) throw new ContestNotFoundError(domainId, tid); RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set)); + // TODO: this is the best way to support old plugins, but need remove one day + if ($set.pids && !$set.problems) { + const mergedScore = { + ...(tdoc.score ? tdoc.score : {}), + ...(tdoc.problems.reduce((acc, cur) => { + if (cur.score) acc[cur.pid] = cur.score; + return acc; + }, {})), + ...($set.score ? $set.score : {}), + }; + $set.problems = $set.pids.map((pid, idx) => ({ + pid, + label: getAlphabeticId(idx), + ...(mergedScore[pid] ? { score: mergedScore[pid] } : {}), + ...($set.balloon + ? ( + typeof $set.balloon[pid] === 'string' ? { + balloon: { + color: $set.balloon[pid], + name: '', + }, + } : { balloon: $set.balloon[pid] } + ) : {}), + })); + } + if (!$set.problems && !$set.pids && ($set.balloon || $set.score)) { + $set.problems = tdoc.problems.map((p) => ({ + ...p, + ...(p.score || ($set.score && $set.score[p.pid]) ? { score: $set.score[p.pid] || p.score } : {}), + ...(p.balloon || ($set.balloon && $set.balloon[p.pid]) ? { + balloon: $set.balloon[p.pid] ? ( + typeof $set.balloon[p.pid] === 'string' ? { + balloon: { + color: $set.balloon[p.pid], + name: '', + }, + } : { balloon: $set.balloon[p.pid] } + ) : p.balloon, + } : {}), + })); + } const res = await document.set(domainId, document.TYPE_CONTEST, tid, $set); await bus.parallel('contest/edit', res); return res; @@ -879,7 +939,7 @@ export async function updateStatus( }: { status?: STATUS, score?: number, subtasks?: Record, lang?: string } = {}, ) { const tdoc = await get(domainId, tid); - if (tdoc.balloon && status === STATUS.STATUS_ACCEPTED) await addBalloon(domainId, tid, uid, rid, pid); + if (tdoc.problems.find((p) => p.pid === pid)?.balloon && status === STATUS.STATUS_ACCEPTED) await addBalloon(domainId, tid, uid, rid, pid); const tsdoc = await document.revPushStatus(tdoc.domainId, document.TYPE_CONTEST, tdoc.docId, uid, 'journal', { rid, pid, status, score, subtasks, lang, }, 'rid'); @@ -993,7 +1053,7 @@ export async function getScoreboard( 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 pdict = await problem.getList(domainId, tdoc.problems.map((p) => p.pid), true, true, problem.PROJECTION_CONTEST_DETAIL); const [rows, udict] = await RULES[tdoc.rule].scoreboard( config, this.translate.bind(this), tdoc, pdict, tsdocsCursor, @@ -1047,6 +1107,37 @@ export const statusText = (tdoc: Tdoc, tsdoc?: any) => ( ? 'Live...' : 'Done'); +export function resolveContestProblemJson(text:string) { + let _problemList = []; + try { + _problemList = JSON.parse(text); + } catch (e) { + throw new ValidationError('problems'); + } + const problems = [] as ContestProblem[]; + const score = {} as Record; + for (const p of _problemList) { + if (!p.pid || typeof p.pid !== 'number') throw new ValidationError('problems'); + if (!p.label || typeof p.label !== 'string') throw new ValidationError('problems'); + if (typeof p.title === 'string' && p.title.length === 0) delete p.title; + if (typeof p.score === 'number') { + if (p.score < 0) throw new ValidationError('problems'); + if (p.score === 100) delete p.score; + score[p.pid] = p.score; + } + problems.push({ + pid: p.pid, + label: p.label, + ...(p.score ? { score: p.score } : {}), + ...(p.title ? { title: p.title } : {}), + }); + } + return { + problems, + score, + }; +} + global.Hydro.model.contest = { RULES, buildContestRule, @@ -1089,4 +1180,5 @@ global.Hydro.model.contest = { isExtended, applyProjection, statusText, + resolveContestProblemJson, }; diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 5b63cc9c70..8b94ad8a93 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import yaml from 'js-yaml'; import { ObjectId } from 'mongodb'; -import { randomstring, sleep } from '@hydrooj/utils'; +import { getAlphabeticId, randomstring, sleep } from '@hydrooj/utils'; import { buildContent } from './lib/content'; import { Logger } from './logger'; import { PERM, PRIV, STATUS } from './model/builtin'; @@ -617,4 +617,27 @@ export const coreScripts: MigrationScript[] = [ await discussion.coll.deleteMany({ content: { $not: { $type: 'string' } } }); return true; }, + async function _95_96() { + await iterateAllDomain(async ({ _id }) => { + logger.info('Processing domain %s', _id); + const tdocs = await contest.getMulti(_id, {}).toArray(); + for (const tdoc of tdocs) { + await contest.edit(_id, tdoc._id, { + problems: tdoc.pids.map((pid, idx) => ({ + pid, + label: getAlphabeticId(idx), + ...(tdoc?.score && tdoc.score[pid] ? { score: tdoc.score[pid] } : {}), + ...(tdoc?.balloon && tdoc.balloon[pid] ? { + balloon: { + name: typeof tdoc.balloon[pid] === 'string' ? '' : tdoc.balloon[pid].name, + color: typeof tdoc.balloon[pid] === 'string' ? tdoc.balloon[pid] : tdoc.balloon[pid].color, + }, + } : {}), + })), + }); + } + logger.info('Domain %s done', _id); + }); + return true; + }, ]; diff --git a/packages/onsite-toolkit/index.ts b/packages/onsite-toolkit/index.ts index 228469cfe9..98c409e9cd 100644 --- a/packages/onsite-toolkit/index.ts +++ b/packages/onsite-toolkit/index.ts @@ -48,18 +48,18 @@ export function apply(ctx: Context) { const teamIds: Record = {}; for (let i = 1; i <= teams.length; i++) teamIds[teams[i - 1].uid] = i; const time = (t: ObjectId) => Math.floor((t.getTimestamp().getTime() - tdoc.beginAt.getTime()) / Time.second); - const pid = (i: number) => String.fromCharCode(65 + i); + const unknownSchool = this.translate('Unknown School'); const submissions = teams.flatMap((i) => { if (!i.journal) return []; - return i.journal.filter((s) => tdoc.pids.includes(s.pid)).map((s) => ({ ...s, uid: i.uid })); + return i.journal.filter((s) => tdoc.problems.find((p) => p.pid === s.pid)).map((s) => ({ ...s, uid: i.uid })); }); this.response.body = { payload: { name: tdoc.title, duration: Math.floor((new Date(tdoc.endAt).getTime() - new Date(tdoc.beginAt).getTime()) / 1000), frozen: Math.floor((new Date(tdoc.lockAt).getTime() - new Date(tdoc.beginAt).getTime()) / 1000), - problems: tdoc.pids.map((i, n) => ({ name: pid(n), id: i.toString() })), + problems: tdoc.problems.map((p) => ({ name: p.label, id: p.pid.toString() })), teams: teams.map((t) => ({ id: t.uid.toString(), name: udict[t.uid].displayName || udict[t.uid].uname, @@ -91,7 +91,7 @@ export function apply(ctx: Context) { type, id: data.id, data, token: `t${token++}`, }); const [pdict, tsdocs] = await Promise.all([ - ProblemModel.getList(tdoc.domainId, tdoc.pids, true, false, ProblemModel.PROJECTION_LIST, true), + ProblemModel.getList(tdoc.domainId, tdoc.problems.map((p) => p.pid), true, false, ProblemModel.PROJECTION_LIST, true), ContestModel.getMultiStatus(tdoc.domainId, { docId: tdoc._id }).toArray(), ]); const udict = await UserModel.getList(tdoc.domainId, tsdocs.map((i) => i.uid)); @@ -192,21 +192,21 @@ export function apply(ctx: Context) { height: 1080, }], })), - ...tdoc.pids.map((i, idx) => getFeed('problems', { - id: `${i}`, + ...tdoc.problems.map((p, idx) => getFeed('problems', { + id: `${p.pid}`, label: String.fromCharCode(65 + idx), - name: pdict[i].title, + name: p.title || pdict[p.pid].title, ordinal: idx, - color: (typeof (tdoc.balloon?.[idx]) === 'object' ? tdoc.balloon[idx].name : tdoc.balloon?.[idx]) || 'white', - rgb: (typeof (tdoc.balloon?.[idx]) === 'object' ? tdoc.balloon[idx].color : null) || '#ffffff', - time_limit: (parseTimeMS((pdict[i].config as ProblemConfig).timeMax) / 1000).toFixed(1), + color: tdoc.problems[idx]?.balloon?.name || 'white', + rgb: tdoc.problems[idx]?.balloon?.color || '#ffffff', + time_limit: (parseTimeMS((pdict[p.pid].config as ProblemConfig).timeMax) / 1000).toFixed(1), test_data_count: 20, })), ]; let cntJudge = 0; const submissions = tsdocs.flatMap((i) => { if (!i.journal) return []; - const journal = i.journal.filter((s) => tdoc.pids.includes(s.pid)); + const journal = i.journal.filter((s) => tdoc.problems.find((p) => p.pid === s.pid)); const result: any[] = []; for (const s of journal) { const submitTime = moment(s.rid.getTimestamp()); diff --git a/packages/scoreboard-xcpcio/index.ts b/packages/scoreboard-xcpcio/index.ts index aa36211209..7385e3d843 100644 --- a/packages/scoreboard-xcpcio/index.ts +++ b/packages/scoreboard-xcpcio/index.ts @@ -63,8 +63,8 @@ export async function apply(ctx: Context) { end_time: Math.floor(tdoc.endAt.getTime() / 1000), frozen_time: tdoc.lockAt ? Math.floor((tdoc.endAt.getTime() - tdoc.lockAt.getTime()) / 1000) : 0, penalty: 1200, - problem_quantity: tdoc.pids.length, - problem_id: tdoc.pids.map((i, idx) => String.fromCharCode(65 + idx)), + problem_quantity: tdoc.problems.length, + problem_id: tdoc.problems.map(({ label }) => label), group: { official: '正式队伍', unofficial: '打星队伍', @@ -83,10 +83,10 @@ export async function apply(ctx: Context) { bronze, }, }, - balloon_color: tdoc.balloon - ? tdoc.pids.filter((i) => tdoc.balloon[i]).map((i) => ({ + balloon_color: tdoc.problems.some((p) => p.balloon) + ? tdoc.problems.filter((p) => p.balloon).map((p) => ({ color: '#000', - background_color: typeof tdoc.balloon[i] === 'string' ? tdoc.balloon[i] : tdoc.balloon[i].color, + background_color: p.balloon.color, })) : [], logo: { @@ -103,7 +103,7 @@ export async function apply(ctx: Context) { const submit = new ObjectId(j.rid as string).getTimestamp().getTime(); const curStatus = status[j.status] || 'SYSTEM_ERROR'; return { - problem_id: tdoc.pids.indexOf(j.pid), + problem_id: tdoc.problems.findIndex((p) => p.pid === j.pid), team_id: `${i.uid}`, timestamp: Math.floor(submit - tdoc.beginAt.getTime()), status: realtime diff --git a/packages/ui-default/backendlib/template.ts b/packages/ui-default/backendlib/template.ts index e4bd8850c7..c1d671c5c5 100644 --- a/packages/ui-default/backendlib/template.ts +++ b/packages/ui-default/backendlib/template.ts @@ -1,6 +1,6 @@ import path from 'path'; import * as status from '@hydrooj/common/status'; -import { findFileSync, getAlphabeticId } from '@hydrooj/utils/lib/utils'; +import { findFileSync, getAlphabeticId, getContestProblemConfig } from '@hydrooj/utils/lib/utils'; import { avatar, Context, difficultyAlgorithm, fs, PERM, PRIV, Service, STATUS, yaml, } from 'hydrooj'; @@ -130,7 +130,7 @@ class Nunjucks extends nunjucks.Environment { this.addGlobal('instanceof', (a, b) => a instanceof b); this.addGlobal('paginate', misc.paginate); this.addGlobal('size', misc.size); - this.addGlobal('utils', { status, getAlphabeticId }); + this.addGlobal('utils', { status, getAlphabeticId, getContestProblemConfig }); this.addGlobal('avatarUrl', avatar); this.addGlobal('formatSeconds', misc.formatSeconds); this.addGlobal('model', global.Hydro.model); diff --git a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx new file mode 100644 index 0000000000..f0726710a2 --- /dev/null +++ b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx @@ -0,0 +1,206 @@ +import { getAlphabeticId } from '@hydrooj/utils/lib/common'; +import React from 'react'; +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; +import { api, i18n } from 'vj/utils'; +import ProblemSelectAutoComplete from '../autocomplete/components/ProblemSelectAutoComplete'; + +export interface Problem { + pid: number; + label: string; + title?: string; + score?: number; + balloon?: { + color: string; + name: string; + }; + _tmpId?: string; // use this as key for better rander +} + +export interface ContestProblemEditorProps { + problems: Problem[]; + onChange: (problems: Problem[]) => void; +} +const randomId = () => Math.random().toString(16).substring(2); +const ContestProblemEditor: React.FC = ({ problems: initialProblems, onChange }) => { + // TODO: also support balloon and other fields in the future + const [problems, setProblems] = React.useState(initialProblems.map((el) => ({ ...el, _tmpId: randomId() }))); + + console.log(problems); + const problemRefs = React.useRef<{ [key: number]: any }>({}); + const [problemRawTitles, setProblemRawTitles] = React.useState>({}); + + const fetchProblemTitles = async (ids: number[]) => { + api('problems', { ids }, ['docId', 'pid', 'title']) + .then((res) => { + setProblemRawTitles(res.reduce((acc, cur) => ({ ...acc, [cur.docId]: cur.title }), {})); + }) + .catch(() => { + // pid maybe not exist + }); + }; + + React.useEffect(() => { + fetchProblemTitles(problems.map((i) => i.pid).filter((i) => i)); + }, []); + + const beforeOnChange = (newProblems: Problem[]) => { + const fixedProblems = newProblems.map((i) => { + const problem = { ...i }; + if (problem.title === '') delete problem.title; + if (problem.score === 100) delete problem.score; + return problem; + }); + setProblems(fixedProblems); + onChange(fixedProblems.map((i) => { + const p = { ...i }; + delete p._tmpId; + return p; + })); + }; + + const handleAdd = () => { + const newProblems = [...problems, { pid: 0, label: getAlphabeticId(problems.length), _tmpId: randomId() }]; + setProblems(newProblems); + beforeOnChange(newProblems); + }; + + const handleRemove = (index: number) => { + const newProblems = problems.filter((_, i) => i !== index); + setProblems(newProblems); + beforeOnChange(newProblems); + }; + + const handleChange = (index: number, field: keyof Problem, value: string | number) => { + const newProblems = [...problems]; + const problem = { ...newProblems[index] }; + + switch (field) { + case 'pid': + problem.pid = Number(value); + break; + default: + problem[field] = value; + } + + newProblems[index] = problem; + setProblems(newProblems); + if (field === 'pid') { + fetchProblemTitles(newProblems.map((i) => i.pid).filter((i) => i)); + } + beforeOnChange(newProblems); + }; + + const onDragEnd = (result) => { + console.log(result); + if (!result.destination) return; + + const newProblems = Array.from(problems); + // exchange label + [ + newProblems[result.source.index], newProblems[result.destination.index], + ] = [ + newProblems[result.destination.index], newProblems[result.source.index], + ]; + const [labelX, labelY] = [newProblems[result.source.index].label, newProblems[result.destination.index].label]; + newProblems[result.source.index].label = labelY; + newProblems[result.destination.index].label = labelX; + + setProblems(newProblems); + beforeOnChange(newProblems); + }; + + return ( +
+ + + + + + + + + + + + + + + {(provided) => ( + + {problems.map((problem, index) => ( + + {(providedDrag, snapshot) => ( + + + + + + + + + + )} + + ))} + {provided.placeholder} + + )} + +
Pid{i18n('Raw Title')}{i18n('Custom Title')}{i18n('Label')}{i18n('Score')}{i18n('Action')}
+ ⋮ + + { problemRefs.current[index] = ref; }} + onChange={(v) => handleChange(index, 'pid', v)} + selectedKeys={[problem.pid.toString()]} + /> + + {problemRawTitles[problem.pid]} + + handleChange(index, 'title', e.target.value)} + placeholder="Empty will use raw title" + /> + + handleChange(index, 'label', e.target.value)} + /> + + handleChange(index, 'score', parseInt(e.target.value, 10) || 0)} + min={0} + /> + + handleRemove(index)}> + {i18n('Remove')} + +
+
+
+ + +
+
+ ); +}; + +export default ContestProblemEditor; diff --git a/packages/ui-default/components/contestProblemEditor/contestproblemeditor.page.styl b/packages/ui-default/components/contestProblemEditor/contestproblemeditor.page.styl new file mode 100644 index 0000000000..7c534cd0be --- /dev/null +++ b/packages/ui-default/components/contestProblemEditor/contestproblemeditor.page.styl @@ -0,0 +1,11 @@ +.page--contest_edit, .page--contest_create, .page--homework_edit, .page--homework_create + .col--drag + width: rem(20px) + .col--pid + width: rem(100px) + .col--label + width: rem(100px) + .col--score + width: rem(100px) + .col--action + width: rem(80px) \ No newline at end of file diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 52868b0f16..2c323f9401 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -35,6 +35,7 @@ "@types/pickadate": "^3.5.35", "@types/qrcode": "^1.5.5", "@types/react": "^18.3.23", + "@types/react-beautiful-dnd": "^13", "@types/react-dom": "^18.3.7", "@types/redux-logger": "^3.0.13", "@types/uuid": "^10.0.0", @@ -140,6 +141,7 @@ "markdown-it-merge-cells": "^2.0.0", "markdown-it-table-of-contents": "^0.9.0", "nunjucks": "^3.2.4", + "react-beautiful-dnd": "^13.1.1", "schemastery-jsonschema": "^1.1.0", "streamsaver": "^2.0.6", "uuid": "^11.1.0", diff --git a/packages/ui-default/pages/contest_balloon.page.tsx b/packages/ui-default/pages/contest_balloon.page.tsx index 29c4aca2a7..abbf6a5600 100644 --- a/packages/ui-default/pages/contest_balloon.page.tsx +++ b/packages/ui-default/pages/contest_balloon.page.tsx @@ -1,5 +1,4 @@ /* eslint-disable react-refresh/only-export-components */ -import { getAlphabeticId } from '@hydrooj/utils/lib/common'; import yaml from 'js-yaml'; import React from 'react'; import { HexColorInput, HexColorPicker } from 'react-colorful'; @@ -26,14 +25,15 @@ function Balloon({ tdoc, val }) { - {tdoc.pids.map((pid) => { + {tdoc.problems.map((cp, idx) => { + const pid = cp.pid; const { color: c, name } = val[+pid]; return ( {now === pid - ? ({getAlphabeticId(tdoc.pids.indexOf(+pid))}) - : ({getAlphabeticId(tdoc.pids.indexOf(+pid))})} + ? ({cp.label}) + : ({cp.label})} { val[+pid].name = e.target.value; }} /> - {tdoc.pids.indexOf(+pid) === 0 && + {idx === 0 && {now && { val[+now].color = e; setColor(e); }} style={{ padding: '1rem' }} />} } @@ -66,8 +66,10 @@ function Balloon({ tdoc, val }) { } async function handleSetColor(tdoc) { - const val = tdoc.balloon || {}; - for (const pid of tdoc.pids) val[+pid] ||= { color: '#ffffff', name: '' }; + const val = {}; + for (const cp of tdoc.problems) { + val[+cp.pid] = cp.balloon || { color: '#ffffff', name: '' }; + } Notification.info(i18n('Loading...')); const action = await new ActionDialog({ $body: tpl(<> diff --git a/packages/ui-default/pages/contest_edit.page.ts b/packages/ui-default/pages/contest_edit.page.ts index 6a6d59d06b..2f9f4902b7 100644 --- a/packages/ui-default/pages/contest_edit.page.ts +++ b/packages/ui-default/pages/contest_edit.page.ts @@ -1,8 +1,11 @@ import $ from 'jquery'; import moment from 'moment'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; import LanguageSelectAutoComplete from 'vj/components/autocomplete/LanguageSelectAutoComplete'; import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete'; import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete'; +import ContestProblemEditor from 'vj/components/contestProblemEditor/ContestProblemEditor'; import { ConfirmDialog } from 'vj/components/dialog'; import { NamedPage } from 'vj/misc/Page'; import { i18n, request, tpl } from 'vj/utils'; @@ -11,6 +14,19 @@ const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true, clearDefaultValue: false }); UserSelectAutoComplete.getOrConstruct($('[name="maintainer"]'), { multi: true, clearDefaultValue: false }); LanguageSelectAutoComplete.getOrConstruct($('[name=langs]'), { multi: true }); + if ($('#problem-editor').length) { + const problemEditor = $('#problem-editor'); + const problemsInput = $('[name=problems]'); + ReactDOM.createRoot(problemEditor[0]).render( + React.createElement(ContestProblemEditor, { + onChange: (problems) => { + console.log(problems); + problemsInput.val(JSON.stringify(problems)); + }, + problems: JSON.parse(problemsInput.val() as string), + }), + ); + } $('[name="rule"]').on('change', () => { const rule = $('[name="rule"]').val(); $('.contest-rule-settings input').attr('disabled', 'disabled'); diff --git a/packages/ui-default/templates/components/contest.html b/packages/ui-default/templates/components/contest.html index 923bbab4a3..fcc639ee46 100644 --- a/packages/ui-default/templates/components/contest.html +++ b/packages/ui-default/templates/components/contest.html @@ -8,5 +8,5 @@ {% endmacro %} {% macro render_clarification_subject(tdoc, pdict, subject) %} -{% if subject == 0 %}{{ _('General Issue') }}{% elif subject == -1 %}{{ _('Technical Issue') }}{% else %}{{ utils.getAlphabeticId(tdoc.pids.indexOf(subject)) }}. {{ pdict[subject].title }}{% endif %} +{% if subject == 0 %}{{ _('General Issue') }}{% elif subject == -1 %}{{ _('Technical Issue') }}{% else %}{% set cp = utils.getContestProblemConfig(subject, tdoc) %}{{ cp.label }}. {{ cp.title|default(pdict[subject].title) }}{% endif %} {% endmacro %} \ No newline at end of file diff --git a/packages/ui-default/templates/components/problem.html b/packages/ui-default/templates/components/problem.html index 5e63aafb38..471828a02d 100644 --- a/packages/ui-default/templates/components/problem.html +++ b/packages/ui-default/templates/components/problem.html @@ -5,11 +5,11 @@ {%- endif -%} {% if show_pid %} -{%- if tdoc and alphabetic -%}{{ utils.getAlphabeticId(tdoc.pids.indexOf(pdoc.docId)) }} +{%- if tdoc and alphabetic -%}{{ utils.getContestProblemConfig(pdoc.docId, tdoc).label }} {%- elif pdoc.pid and pdoc.pid.includes('-') -%}{{ pdoc.pid.split('-').join('#') }} {%- else -%}{{ pdoc.pid|default(pdoc.docId) }}{%- endif -%} {% endif %} -{%- if not small -%}{% if show_pid %}  {% endif %}{{ pdoc.title }}{%- endif -%} +{%- if not small -%}{% if show_pid %}  {% endif %}{%- if tdoc -%}{{utils.getContestProblemConfig(pdoc.docId, tdoc).title|default(pdoc.title)}}{%- else -%}{{ pdoc.title }}{%- endif -%}{%- endif -%} {%- if not invalid -%} {%- endif -%} diff --git a/packages/ui-default/templates/contest_balloon.html b/packages/ui-default/templates/contest_balloon.html index 771b44e587..a151baf18f 100644 --- a/packages/ui-default/templates/contest_balloon.html +++ b/packages/ui-default/templates/contest_balloon.html @@ -13,7 +13,7 @@

{{ _('Balloon Status') }}

{{ noscript_note.render() }}
- {% if not tdoc.balloon|length %} + {% if tdoc.problems|length===0 or not tdoc.problems[0].balloon %} {{ nothing.render('Please set the balloon color for each problem first.') }} {% else %} diff --git a/packages/ui-default/templates/contest_edit.html b/packages/ui-default/templates/contest_edit.html index 903213ec03..abdf99be6b 100644 --- a/packages/ui-default/templates/contest_edit.html +++ b/packages/ui-default/templates/contest_edit.html @@ -63,14 +63,36 @@

{{ _('Basic Info') }}

row:false }) }} - {{ form.form_text({ - columns:12, - label:'Problems', - name:'pids', - value:pids, - placeholder:_("Seperated with ','"), - row:true - }) }} +
+
+ +
+ +
+
+ + + + + + + + + + {% for p in problems %} + + + + + + + {% endfor %} + +
{{ _('Problem') }}{{ _('Label') }}{{ _('Score') }}{{ _('Custom Title') }}
{{ p.pid }}{{ p.label }}{{ p.score|default(100) }}{{ p.title }}
+
+ + + {{ form.form_textarea({ columns:null, label:'Description', diff --git a/packages/ui-default/templates/contest_manage.html b/packages/ui-default/templates/contest_manage.html index 4753a4184d..b63b4f6b60 100644 --- a/packages/ui-default/templates/contest_manage.html +++ b/packages/ui-default/templates/contest_manage.html @@ -21,15 +21,15 @@ - {%- for pid in tdoc.pids -%} + {%- for cp in tdoc.problems -%} - - {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, invalid=true, alphabetic=true) }} + + {{ problem.render_problem_title(pdict[cp.pid], tdoc=tdoc, invalid=true, alphabetic=true) }} - {{ tdoc.score[pid]|default(100) }} + {{ cp.score|default(100) }} {%- endfor -%} @@ -126,8 +126,8 @@

{{ _('Send Broadcast Message') }}

diff --git a/packages/ui-default/templates/contest_problemlist.html b/packages/ui-default/templates/contest_problemlist.html index 8f5b8e90f7..c1bd729e59 100644 --- a/packages/ui-default/templates/contest_problemlist.html +++ b/packages/ui-default/templates/contest_problemlist.html @@ -41,13 +41,14 @@

{{ _('Problems') }}

- {%- for pid in tdoc.pids -%} + {%- for cp in tdoc.problems -%} + {% set pid = cp.pid %} {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} {% if psdict[pid] and psdict[pid].rid %} {% set rdoc = rdict[psdict[pid].rid] %} {% if model.contest.canShowSelfRecord.call(handler, tdoc) %} - {% set displayScore = ((tdoc.score[pid]|default(100)) * (psdict[pid].score|default(0)) / 100) if showScore else (psdict[pid].penaltyScore|default(psdict[pid].score)) %} + {% set displayScore = ((cp.score|default(100)) * (psdict[pid].score|default(0)) / 100) if showScore else (psdict[pid].penaltyScore|default(psdict[pid].score)) %} {{ record.render_status_td(rdoc, displayScore=displayScore) }} {% else %} {{ _('Submitted') }} @@ -70,7 +71,7 @@

{{ _('Problems') }}

{% endif %} {% endif %} {% if showScore %} - {{ tdoc.score[pid]|default(100) }} + {{ cp.score|default(100) }} {% endif %} {{ problem.render_problem_title(pdict[pid], tdoc, alphabetic=true) }} @@ -184,8 +185,8 @@

{{ _('Send Clarification Request') }}

diff --git a/packages/ui-default/templates/homework_detail.html b/packages/ui-default/templates/homework_detail.html index b7bae2123b..3983efae14 100644 --- a/packages/ui-default/templates/homework_detail.html +++ b/packages/ui-default/templates/homework_detail.html @@ -40,7 +40,8 @@

{{ _('Problem') }}

{% set isAdmin = handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD) %} {% set ntdoc = model.contest.isDone(tdoc) or (tsdoc.attend and not model.contest.isNotStarted(tdoc)) %} - {%- for pid in tdoc.pids -%} + {%- for cp in tdoc.problems -%} + {% set pid = cp.pid %} {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} {% if psdict[pid] and psdict[pid].rid %} @@ -58,9 +59,9 @@

{{ _('Problem') }}

{% endif %} {% if isAdmin and not ntdoc %} - {{ problem.render_problem_title(pdict[pid], show_invisible_flag=false, show_tags=false) }} + {{ problem.render_problem_title(pdict[pid], show_invisible_flag=false, show_tags=false, alphabetic=true) }} {% else %} - {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false) }} + {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false, alphabetic=true) }} {% endif %} diff --git a/packages/ui-default/templates/homework_edit.html b/packages/ui-default/templates/homework_edit.html index b29aaf9a77..78a89173cf 100644 --- a/packages/ui-default/templates/homework_edit.html +++ b/packages/ui-default/templates/homework_edit.html @@ -84,12 +84,36 @@ - {{ form.form_text({ - columns:null, - label:'Problems', - name:'pids', - value:pids - }) }} +
+
+ +
+ +
+ + + + + + + + + + + {% for p in problems %} + + + + + + + {% endfor %} + +
{{ _('Problem') }}{{ _('Label') }}{{ _('Score') }}{{ _('Custom Title') }}
{{ p.pid }}{{ p.label }}{{ p.score|default(100) }}{{ p.title }}
+
+
+
+
{{ form.form_textarea({ columns:null, label:'Content', diff --git a/packages/ui-default/templates/partials/contest_balloon.html b/packages/ui-default/templates/partials/contest_balloon.html index d39462f4b2..b30d89f90d 100644 --- a/packages/ui-default/templates/partials/contest_balloon.html +++ b/packages/ui-default/templates/partials/contest_balloon.html @@ -25,7 +25,7 @@ {% endif %} - {{ utils.getAlphabeticId(tdoc.pids.indexOf(bdoc.pid)) }}  ({{ tdoc.balloon[bdoc.pid].name }}) + {{ utils.getContestProblemConfig(bdoc.pid, tdoc).label }}  ({{ tdoc.balloon[bdoc.pid].name }}) {{ user.render_inline(udict[bdoc.uid], badge=false) }} diff --git a/packages/ui-default/templates/partials/contest_sidebar.html b/packages/ui-default/templates/partials/contest_sidebar.html index e541ee3bb6..11550082d9 100644 --- a/packages/ui-default/templates/partials/contest_sidebar.html +++ b/packages/ui-default/templates/partials/contest_sidebar.html @@ -117,7 +117,7 @@

{{ tdoc.title }}

{% endif %}
{{ _('Rule') }}
{{ _(model.contest.RULES[tdoc.rule].TEXT) }}
-
{{ _('Problem') }}
{{ tdoc.pids.length }}
+
{{ _('Problem') }}
{{ tdoc.problems.length }}
{{ _('Start at') }}
{{ contest.render_time(tsdoc.startAt or tdoc.beginAt) }}
{{ _('End at') }}
{{ contest.render_time(tsdoc.endAt or tdoc.endAt) }}
{{ _('Duration') }}
{{ contest.render_duration(tdoc) }} {{ _('hour(s)') }}
diff --git a/packages/ui-default/templates/partials/contest_sidebar_management.html b/packages/ui-default/templates/partials/contest_sidebar_management.html index 1114449f35..2372be175c 100644 --- a/packages/ui-default/templates/partials/contest_sidebar_management.html +++ b/packages/ui-default/templates/partials/contest_sidebar_management.html @@ -42,7 +42,7 @@

{{ tdoc.title }}

{% endif %}
{{ _('Rule') }}
{{ model.contest.RULES[tdoc.rule].TEXT }}
-
{{ _('Problem') }}
{{ tdoc.pids.length }}
+
{{ _('Problem') }}
{{ tdoc.problems.length }}
{{ _('Start at') }}
{{ contest.render_time(tsdoc.startAt or tdoc.beginAt) }}
{{ _('End at') }}
{{ contest.render_time(tsdoc.endAt or tdoc.endAt) }}
{{ _('Duration') }}
{{ contest.render_duration(tdoc) }} {{ _('hour(s)') }}
diff --git a/packages/ui-default/templates/problem_detail.html b/packages/ui-default/templates/problem_detail.html index 81429dd4ab..7cda284b65 100644 --- a/packages/ui-default/templates/problem_detail.html +++ b/packages/ui-default/templates/problem_detail.html @@ -64,23 +64,24 @@

{% endif %} {%- if tdoc -%} - {{ utils.getAlphabeticId(tdoc.pids.indexOf(pdoc.docId)) }} + {{ utils.getContestProblemConfig(pdoc.docId, tdoc).label }} {%- elif pdoc.pid and pdoc.pid.includes('-') -%} {{ pdoc.pid.split('-').join('#') }} {%- else -%} #{{ pdoc.pid|default(pdoc.docId) }} - {%- endif -%}. {{ pdoc.title }} + {%- endif -%}. {%- if tdoc -%}{{ utils.getContestProblemConfig(pdoc.docId, tdoc).title|default(pdoc.title) }}{%- else -%}{{ pdoc.title }}{%- endif -%}

- {% if tdoc and tdoc.pids|length <= 26 %} + {% if tdoc and tdoc.problems|length <= 26 %}
- {% for pid in tdoc.pids -%} + {% for cp in tdoc.problems -%} + {% set pid = cp.pid %} {% set status = tsdoc.detail[pid].status %} {% set pass = status == STATUS.STATUS_ACCEPTED %} {% set fail = status and not pass %} - {{ utils.getAlphabeticId(loop.index0) }} + {{ cp.label }} {% if status %}{% endif %} {%- endfor %} diff --git a/packages/ui-default/templates/record_main.html b/packages/ui-default/templates/record_main.html index f937bee1e8..8e948ff0f4 100644 --- a/packages/ui-default/templates/record_main.html +++ b/packages/ui-default/templates/record_main.html @@ -109,7 +109,7 @@

{{ _('Filter') }}

(('&tid=' + filterTid) if filterTid else '') + (('&uidOrName=' + filterUidOrName) if filterUidOrName else '') + (('&lang=' + filterLang) if filterLang else '') - + (('&status=' + filterStatus) if filterStatus else '') + + (('&status=' + filterStatus) if filterStatus or filterStatus===0 else '') + (('&pid=' + filterPid) if filterPid else '') + ('&all=1' if all else '') + ('&allDomain=1' if allDomain else '') }}">{{ _('pager_next') }} diff --git a/packages/ui-default/templates/record_main_tr.html b/packages/ui-default/templates/record_main_tr.html index f05b8994d8..6ebd7f5726 100644 --- a/packages/ui-default/templates/record_main_tr.html +++ b/packages/ui-default/templates/record_main_tr.html @@ -17,7 +17,9 @@ | {% endif %} - {% if pdoc and rdoc.contest %} + {% if tdoc %} + {{ problem.render_problem_title(pdoc, tdoc=tdoc, show_tags=false, show_invisible_flag=false, alphabetic=true) }} + {% elif pdoc and rdoc.contest %} {{ problem.render_problem_title(pdoc, tdoc=tdoc, show_tags=false, show_invisible_flag=false) }} {% elif pdoc and (not pdoc.hidden or handler.user.hasPerm(perm.PERM_VIEW_PROBLEM_HIDDEN) or handler.user.own(pdoc)) %} {{ problem.render_problem_title(pdoc, show_tags=false) }} diff --git a/packages/utils/lib/common.ts b/packages/utils/lib/common.ts index 99f1bd23c8..6fcba6381f 100644 --- a/packages/utils/lib/common.ts +++ b/packages/utils/lib/common.ts @@ -245,3 +245,5 @@ export const getAlphabeticId = (() => { for (const ch of alphabet) cache.push(...alphabet.split('').map((c) => ch + c)); return (i: number) => cache[i] || (i < 0 ? '?' : f(i)); })(); + +export const getContestProblemConfig = (pid, tdoc) => tdoc.problems.find((cp) => cp.pid === pid); From c6d2002f9264f2b742bf8c4bc11a5f7c41bcc550 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Wed, 16 Jul 2025 23:12:58 +0800 Subject: [PATCH 02/27] small fix --- packages/hydrooj/locales/zh.yaml | 1 + packages/ui-default/components/autocomplete/index.tsx | 2 +- .../components/contestProblemEditor/ContestProblemEditor.tsx | 2 +- packages/ui-default/pages/contest_edit.page.ts | 1 - 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index 1a9e3cbb47..99d6f99217 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -321,6 +321,7 @@ Edit training plans: 修改训练计划 Edit: 编辑 Email Visibility: Email 可见性 Email: 电子邮件 +Empty will use raw title: 留空以使用原标题 Enabled: 开启 End at: 结束于 End Date: 结束日期 diff --git a/packages/ui-default/components/autocomplete/index.tsx b/packages/ui-default/components/autocomplete/index.tsx index 6f67a9124e..8bd4559e2d 100644 --- a/packages/ui-default/components/autocomplete/index.tsx +++ b/packages/ui-default/components/autocomplete/index.tsx @@ -73,7 +73,7 @@ export default class AutoComplete = {}, Mult setValue(v); this.onChange(v); }} - selectedKeys={(Array.isArray(value) ? value : value.split(',')).map((i) => i.trim()).filter((i) => i)} + selectedKeys={(Array.isArray(value) ? value : (value || '').split(',')).map((i) => i.trim()).filter((i) => i)} height="34px" {...this.options.props} />; diff --git a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx index f0726710a2..6e2178ee5f 100644 --- a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx +++ b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx @@ -158,7 +158,7 @@ const ContestProblemEditor: React.FC = ({ problems: i className="textbox" value={problem.title || ''} onChange={(e) => handleChange(index, 'title', e.target.value)} - placeholder="Empty will use raw title" + placeholder={i18n('Empty will use raw title')} /> diff --git a/packages/ui-default/pages/contest_edit.page.ts b/packages/ui-default/pages/contest_edit.page.ts index 2f9f4902b7..17bea54876 100644 --- a/packages/ui-default/pages/contest_edit.page.ts +++ b/packages/ui-default/pages/contest_edit.page.ts @@ -20,7 +20,6 @@ const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', ReactDOM.createRoot(problemEditor[0]).render( React.createElement(ContestProblemEditor, { onChange: (problems) => { - console.log(problems); problemsInput.val(JSON.stringify(problems)); }, problems: JSON.parse(problemsInput.val() as string), From 2355c1fd49f4f8cfa23c7c349bca9a077ecbad3a Mon Sep 17 00:00:00 2001 From: Bhscer Date: Wed, 16 Jul 2025 23:51:22 +0800 Subject: [PATCH 03/27] fix the problem of dnd --- .../components/contestProblemEditor/ContestProblemEditor.tsx | 5 ++--- packages/ui-default/package.json | 3 +-- packages/ui-default/templates/contest_manage.html | 2 +- packages/ui-default/templates/record_main.html | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx index 6e2178ee5f..6012570dd6 100644 --- a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx +++ b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx @@ -1,6 +1,6 @@ import { getAlphabeticId } from '@hydrooj/utils/lib/common'; +import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; import React from 'react'; -import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; import { api, i18n } from 'vj/utils'; import ProblemSelectAutoComplete from '../autocomplete/components/ProblemSelectAutoComplete'; @@ -25,7 +25,6 @@ const ContestProblemEditor: React.FC = ({ problems: i // TODO: also support balloon and other fields in the future const [problems, setProblems] = React.useState(initialProblems.map((el) => ({ ...el, _tmpId: randomId() }))); - console.log(problems); const problemRefs = React.useRef<{ [key: number]: any }>({}); const [problemRawTitles, setProblemRawTitles] = React.useState>({}); @@ -79,7 +78,7 @@ const ContestProblemEditor: React.FC = ({ problems: i problem.pid = Number(value); break; default: - problem[field] = value; + (problem as any)[field] = value; } newProblems[index] = problem; diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 2c323f9401..af10f25e44 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -35,7 +35,6 @@ "@types/pickadate": "^3.5.35", "@types/qrcode": "^1.5.5", "@types/react": "^18.3.23", - "@types/react-beautiful-dnd": "^13", "@types/react-dom": "^18.3.7", "@types/redux-logger": "^3.0.13", "@types/uuid": "^10.0.0", @@ -122,6 +121,7 @@ "webpackbar": "^7.0.0" }, "dependencies": { + "@hello-pangea/dnd": "^18.0.1", "@hydrooj/common": "workspace:^", "ansi_up": "^6.0.6", "cssfilter": "^0.0.11", @@ -141,7 +141,6 @@ "markdown-it-merge-cells": "^2.0.0", "markdown-it-table-of-contents": "^0.9.0", "nunjucks": "^3.2.4", - "react-beautiful-dnd": "^13.1.1", "schemastery-jsonschema": "^1.1.0", "streamsaver": "^2.0.6", "uuid": "^11.1.0", diff --git a/packages/ui-default/templates/contest_manage.html b/packages/ui-default/templates/contest_manage.html index b63b4f6b60..a605f22bf5 100644 --- a/packages/ui-default/templates/contest_manage.html +++ b/packages/ui-default/templates/contest_manage.html @@ -127,7 +127,7 @@

{{ _('Send Broadcast Message') }}

{% for cp in tdoc.problems %} - + {% endfor %} diff --git a/packages/ui-default/templates/record_main.html b/packages/ui-default/templates/record_main.html index 8e948ff0f4..4efdf1891b 100644 --- a/packages/ui-default/templates/record_main.html +++ b/packages/ui-default/templates/record_main.html @@ -109,7 +109,7 @@

{{ _('Filter') }}

(('&tid=' + filterTid) if filterTid else '') + (('&uidOrName=' + filterUidOrName) if filterUidOrName else '') + (('&lang=' + filterLang) if filterLang else '') - + (('&status=' + filterStatus) if filterStatus or filterStatus===0 else '') + + (('&status=' + filterStatus) if filterStatus or filterStatus == 0 else '') + (('&pid=' + filterPid) if filterPid else '') + ('&all=1' if all else '') + ('&allDomain=1' if allDomain else '') }}">{{ _('pager_next') }} From 74a181bf0b45e1b7ad6bdaa5075989e56b7e3d85 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Thu, 17 Jul 2025 13:23:18 +0800 Subject: [PATCH 04/27] add a record column to optimize the speed --- packages/hydrooj/src/handler/problem.ts | 2 +- packages/hydrooj/src/interface.ts | 1 + packages/hydrooj/src/model/contest.ts | 71 ++++++++++++++++--------- packages/hydrooj/src/upgrade.ts | 5 ++ packages/utils/lib/common.ts | 2 +- 5 files changed, 54 insertions(+), 27 deletions(-) diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 5b2d71eb5e..82349b4dd4 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -304,7 +304,7 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler { if (this.pdoc.config.langs) t.push(this.pdoc.config.langs); if (ddoc.langs) t.push(ddoc.langs.split(',').map((i) => i.trim()).filter((i) => i)); if (this.domain.langs) t.push(this.domain.langs.split(',').map((i) => i.trim()).filter((i) => i)); - if (this.tdoc?.langs) t.push(this.tdoc.langs); + if (this.tdoc?.langs && this.tdoc?.langs.length) t.push(this.tdoc.langs); if (this.pdoc.config.type === 'remote_judge') { const p = this.pdoc.config.subType; const dl = Object.keys(setting.langs).filter((i) => i.startsWith(`${p}.`) || setting.langs[i].validAs[p]); diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 7ed77dfaef..43a999c641 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -266,6 +266,7 @@ export interface Tdoc extends Document { rule: string; pids: number[]; problems: ContestProblem[]; + pid2idx: Record; rated?: boolean; _code?: string; assign?: string[]; diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 0461b1a9c1..ae4ce9d066 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -1,5 +1,6 @@ import { sumBy } from 'lodash'; import { Filter, ObjectId } from 'mongodb'; +import Schema from 'schemastery'; import { Counter, formatSeconds, getAlphabeticId, Time, } from '@hydrooj/utils/lib/utils'; @@ -103,7 +104,7 @@ const acm = buildContestRule({ let time = 0; const lockAt = isLocked(tdoc) ? tdoc.lockAt : null; for (const j of journal) { - if (!tdoc.problems.find((p) => p.pid === j.pid)) continue; + if (!Object.hasOwn(tdoc.pid2idx, j.pid)) continue; if (!this.submitAfterAccept && display[j.pid]?.status === STATUS.STATUS_ACCEPTED) continue; if (![STATUS.STATUS_ACCEPTED, STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_FORMAT_ERROR, STATUS.STATUS_CANCELED].includes(j.status)) { naccept[j.pid]++; @@ -144,7 +145,8 @@ const acm = buildContestRule({ } columns.push({ type: 'solved', value: `${_('Solved')}\n${_('Total Time')}` }); for (let i = 1; i <= tdoc.problems.length; i++) { - const pid = tdoc.problems[i - 1].pid; + const cp = tdoc.problems[i - 1]; + const pid = cp.pid; pdict[pid].nAccept = pdict[pid].nSubmit = 0; if (config.isExport) { columns.push( @@ -160,7 +162,7 @@ const acm = buildContestRule({ } else { columns.push({ type: 'problem', - value: getAlphabeticId(i - 1), + value: cp.label, raw: pid, }); } @@ -284,7 +286,7 @@ const oi = buildContestRule({ let score = 0; const lockAt = isLocked(tdoc) ? tdoc.lockAt : null; - for (const j of journal.filter((i) => tdoc.problems.find((p) => p.pid === i.pid))) { + for (const j of journal.filter((i) => Object.hasOwn(tdoc.pid2idx, i.pid))) { if (lockAt && j.rid.getTimestamp() > lockAt) { npending[j.pid]++; display[j.pid] ||= {}; @@ -297,7 +299,7 @@ const oi = buildContestRule({ } } for (const i in display) { - score += ((tdoc.problems.find((p) => p.pid.toString() === i)?.score || 100) * (display[i].score || 0)) / 100; + score += ((tdoc.problems?.[tdoc.pid2idx?.[Number(i)]]?.score || 100) * (display[i].score || 0)) / 100; } return { score, detail, display }; }, @@ -329,7 +331,7 @@ const oi = buildContestRule({ columns.push({ type: 'problem', value: cp.label, - raw: cp, + raw: cp.pid, }); } } @@ -353,8 +355,10 @@ const oi = buildContestRule({ row.push({ type: 'total_score', value: tsdoc.score || 0 }); const accepted = {}; for (const s of tsdoc.journal || []) { + // console.log(typeof s.pid, s.pid, !!pdict[s.pid]); if (!pdict[s.pid]) continue; if (config.lockAt && s.rid.getTimestamp() > config.lockAt) continue; + // console.log(s.pid, pdict[s.pid].nSubmit) pdict[s.pid].nSubmit++; if (s.status === STATUS.STATUS_ACCEPTED && !accepted[s.pid]) { pdict[s.pid].nAccept++; @@ -481,7 +485,7 @@ const strictioi = buildContestRule({ j.status = Math.max(...Object.values(subtasks[j.pid]).map((i) => i.status)); if (!detail[j.pid] || detail[j.pid].score < j.score) detail[j.pid] = { ...j, subtasks: subtasks[j.pid] }; } - for (const i in detail) score += ((tdoc.problems.find((p) => p.pid.toString() === i)?.score || 100) * (detail[i].score || 0)) / 100; + for (const i in detail) score += ((tdoc.problems?.[tdoc.pid2idx?.[Number(i)]]?.score || 100) * (detail[i].score || 0)) / 100; return { score, detail }; }, async scoreboardRow(config, _, tdoc, pdict, udoc, rank, tsdoc, meta) { @@ -509,6 +513,7 @@ const strictioi = buildContestRule({ for (const cp of tdoc.problems) { const pid = cp.pid; const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`; + const fullMark = tdoc.problems?.[tdoc.pid2idx?.[pid]]?.score || 100; const n: ScoreboardNode = (!config.isExport && !config.lockAt && isDone(tdoc) && meta?.psdict?.[index]?.rid && tsddict[pid]?.rid?.toHexString() !== meta?.psdict?.[index]?.rid?.toHexString() @@ -517,19 +522,19 @@ const strictioi = buildContestRule({ type: 'records', value: '', raw: [{ - value: ((tsddict[pid]?.score || 0) * ((tdoc.problems.find((p) => p.pid === pid)?.score || 100) / 100)).toString() || '', + value: ((tsddict[pid]?.score || 0) * (fullMark / 100)).toString() || '', raw: tsddict[pid]?.rid || null, score: tsddict[pid]?.score, }, { value: ( - (meta?.psdict?.[index]?.score || 0) * ((tdoc.problems.find((p) => p.pid === pid)?.score || 100) / 100) + (meta?.psdict?.[index]?.score || 0) * (fullMark / 100) ).toString() || '', raw: meta?.psdict?.[index]?.rid ?? null, score: meta?.psdict?.[index]?.score, }], } : { type: 'record', - value: ((tsddict[pid]?.score || 0) * ((tdoc.problems.find((p) => p.pid === pid)?.score || 100) / 100)).toString() || '', + value: ((tsddict[pid]?.score || 0) * (fullMark / 100)).toString() || '', raw: tsddict[pid]?.rid, score: tsddict[pid]?.score, }; @@ -571,7 +576,7 @@ const ledo = buildContestRule({ for (const cp of tdoc.problems) { const pid = cp.pid; if (!detail[pid]) continue; - const rate = (tdoc.problems.find((p) => p.pid === pid)?.score || 100) / 100; + const rate = (tdoc.problems?.[tdoc.pid2idx?.[pid]]?.score || 100) / 100; score += detail[pid].penaltyScore * rate; originalScore += detail[pid].score * rate; } @@ -609,7 +614,7 @@ const ledo = buildContestRule({ const pid = cp.pid; row.push({ type: 'record', - value: ((tsddict[pid]?.penaltyScore || 0) * ((tdoc.problems.find((p) => p.pid === pid)?.score || 100) / 100)).toString(), + value: ((tsddict[pid]?.penaltyScore || 0) * ((tdoc.problems?.[tdoc.pid2idx?.[pid]]?.score || 100) / 100)).toString(), hover: tsddict[pid]?.ntry ? `-${tsddict[pid].ntry} (${Math.round(Math.max(0.7, 0.95 ** tsddict[pid].ntry) * 100)}%)` : '', raw: tsddict[pid]?.rid, score: tsddict[pid]?.score, @@ -635,7 +640,7 @@ const homework = buildContestRule({ stat: (tdoc, journal) => { const effective = {}; for (const j of journal) { - if (tdoc.problems.find((p) => p.pid === j.pid)) effective[j.pid] = j; + if (Object.hasOwn(tdoc.pid2idx, j.pid)) effective[j.pid] = j; } function time(jdoc) { const real = (jdoc.rid.getTimestamp().getTime() - tdoc.beginAt.getTime()) / 1000; @@ -643,7 +648,7 @@ const homework = buildContestRule({ } function penaltyScore(jdoc) { - const rate = (tdoc.problems.find((p) => p.pid.toString() === jdoc.pid)?.score || 100) / 100; + const rate = (tdoc.problems?.[tdoc.pid2idx?.[Number(jdoc.pid)]]?.score || 100) / 100; const exceedSeconds = Math.floor( (jdoc.rid.getTimestamp().getTime() - tdoc.penaltySince.getTime()) / 1000, ); @@ -817,13 +822,17 @@ export async function add( ...(data?.score && data.score[pid] ? { score: data.score[pid] } : {}), })); } + const pid2idx = {}; + for (let i = 0; i < problems.length; i++) { + pid2idx[problems[i].pid] = i; + } Object.assign(data, { - content, owner, title, rule, beginAt, endAt, pids, problems, attend: 0, + content, owner, title, rule, beginAt, endAt, pids, problems, pid2idx, attend: 0, }); RULES[rule].check(data); await bus.parallel('contest/before-add', data); const docId = await document.add(domainId, content, owner, document.TYPE_CONTEST, null, null, null, { - assign: [], ...data, title, rule, beginAt, endAt, pids, problems, attend: 0, rated, + assign: [], ...data, title, rule, beginAt, endAt, pids, problems, pid2idx, attend: 0, rated, }); await bus.parallel('contest/add', data, docId); return docId; @@ -875,6 +884,12 @@ export async function edit(domainId: string, tid: ObjectId, $set: Partial) } : {}), })); } + if ($set.problems) { + $set.pid2idx = {}; + for (let i = 0; i < $set.problems.length; i++) { + $set.pid2idx[$set.problems[i].pid] = i; + } + } const res = await document.set(domainId, document.TYPE_CONTEST, tid, $set); await bus.parallel('contest/edit', res); return res; @@ -1108,28 +1123,34 @@ export const statusText = (tdoc: Tdoc, tsdoc?: any) => ( : 'Done'); export function resolveContestProblemJson(text:string) { + const validate = Schema.array(Schema.object({ + pid: Schema.number().required(), + label: Schema.string().required(), + title: Schema.string(), + score: Schema.number().min(0), + balloon: Schema.object({ + color: Schema.string(), + name: Schema.string(), + }), + })).required(); + let _problemList = []; try { - _problemList = JSON.parse(text); + _problemList = validate(JSON.parse(text)); } catch (e) { throw new ValidationError('problems'); } const problems = [] as ContestProblem[]; const score = {} as Record; for (const p of _problemList) { - if (!p.pid || typeof p.pid !== 'number') throw new ValidationError('problems'); - if (!p.label || typeof p.label !== 'string') throw new ValidationError('problems'); - if (typeof p.title === 'string' && p.title.length === 0) delete p.title; - if (typeof p.score === 'number') { - if (p.score < 0) throw new ValidationError('problems'); - if (p.score === 100) delete p.score; + if (typeof p.score === 'number' && p.score !== 100) { score[p.pid] = p.score; } problems.push({ pid: p.pid, label: p.label, - ...(p.score ? { score: p.score } : {}), - ...(p.title ? { title: p.title } : {}), + ...(p.score && p.score !== 100 ? { score: p.score } : {}), + ...(p.title && p.title.length > 0 ? { title: p.title } : {}), }); } return { diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 8b94ad8a93..0f8a728c61 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -622,6 +622,10 @@ export const coreScripts: MigrationScript[] = [ logger.info('Processing domain %s', _id); const tdocs = await contest.getMulti(_id, {}).toArray(); for (const tdoc of tdocs) { + const pid2idx = {}; + for (let i = 0; i < tdoc.pids.length; i++) { + pid2idx[tdoc.pids[i]] = i; + } await contest.edit(_id, tdoc._id, { problems: tdoc.pids.map((pid, idx) => ({ pid, @@ -634,6 +638,7 @@ export const coreScripts: MigrationScript[] = [ }, } : {}), })), + pid2idx, }); } logger.info('Domain %s done', _id); diff --git a/packages/utils/lib/common.ts b/packages/utils/lib/common.ts index 6fcba6381f..5ac6604e83 100644 --- a/packages/utils/lib/common.ts +++ b/packages/utils/lib/common.ts @@ -246,4 +246,4 @@ export const getAlphabeticId = (() => { return (i: number) => cache[i] || (i < 0 ? '?' : f(i)); })(); -export const getContestProblemConfig = (pid, tdoc) => tdoc.problems.find((cp) => cp.pid === pid); +export const getContestProblemConfig = (pid, tdoc) => tdoc.problems[tdoc.pid2idx?.[pid]]; From 9855dbd4133123fe31372f3e73dedf8e9d80c4ea Mon Sep 17 00:00:00 2001 From: Bhscer Date: Thu, 17 Jul 2025 13:31:39 +0800 Subject: [PATCH 05/27] remove some needless checks --- packages/hydrooj/src/model/contest.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index ae4ce9d066..ebc4b37bcd 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -299,7 +299,7 @@ const oi = buildContestRule({ } } for (const i in display) { - score += ((tdoc.problems?.[tdoc.pid2idx?.[Number(i)]]?.score || 100) * (display[i].score || 0)) / 100; + score += ((tdoc.problems[tdoc.pid2idx[Number(i)]]?.score || 100) * (display[i].score || 0)) / 100; } return { score, detail, display }; }, @@ -485,7 +485,7 @@ const strictioi = buildContestRule({ j.status = Math.max(...Object.values(subtasks[j.pid]).map((i) => i.status)); if (!detail[j.pid] || detail[j.pid].score < j.score) detail[j.pid] = { ...j, subtasks: subtasks[j.pid] }; } - for (const i in detail) score += ((tdoc.problems?.[tdoc.pid2idx?.[Number(i)]]?.score || 100) * (detail[i].score || 0)) / 100; + for (const i in detail) score += ((tdoc.problems[tdoc.pid2idx[Number(i)]]?.score || 100) * (detail[i].score || 0)) / 100; return { score, detail }; }, async scoreboardRow(config, _, tdoc, pdict, udoc, rank, tsdoc, meta) { @@ -513,7 +513,7 @@ const strictioi = buildContestRule({ for (const cp of tdoc.problems) { const pid = cp.pid; const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`; - const fullMark = tdoc.problems?.[tdoc.pid2idx?.[pid]]?.score || 100; + const fullMark = tdoc.problems[tdoc.pid2idx[pid]]?.score || 100; const n: ScoreboardNode = (!config.isExport && !config.lockAt && isDone(tdoc) && meta?.psdict?.[index]?.rid && tsddict[pid]?.rid?.toHexString() !== meta?.psdict?.[index]?.rid?.toHexString() @@ -576,7 +576,7 @@ const ledo = buildContestRule({ for (const cp of tdoc.problems) { const pid = cp.pid; if (!detail[pid]) continue; - const rate = (tdoc.problems?.[tdoc.pid2idx?.[pid]]?.score || 100) / 100; + const rate = (tdoc.problems[tdoc.pid2idx[pid]]?.score || 100) / 100; score += detail[pid].penaltyScore * rate; originalScore += detail[pid].score * rate; } @@ -614,7 +614,7 @@ const ledo = buildContestRule({ const pid = cp.pid; row.push({ type: 'record', - value: ((tsddict[pid]?.penaltyScore || 0) * ((tdoc.problems?.[tdoc.pid2idx?.[pid]]?.score || 100) / 100)).toString(), + value: ((tsddict[pid]?.penaltyScore || 0) * ((tdoc.problems[tdoc.pid2idx[pid]]?.score || 100) / 100)).toString(), hover: tsddict[pid]?.ntry ? `-${tsddict[pid].ntry} (${Math.round(Math.max(0.7, 0.95 ** tsddict[pid].ntry) * 100)}%)` : '', raw: tsddict[pid]?.rid, score: tsddict[pid]?.score, @@ -648,7 +648,7 @@ const homework = buildContestRule({ } function penaltyScore(jdoc) { - const rate = (tdoc.problems?.[tdoc.pid2idx?.[Number(jdoc.pid)]]?.score || 100) / 100; + const rate = (tdoc.problems[tdoc.pid2idx[Number(jdoc.pid)]]?.score || 100) / 100; const exceedSeconds = Math.floor( (jdoc.rid.getTimestamp().getTime() - tdoc.penaltySince.getTime()) / 1000, ); From 137c186373f1dfbde095062cde28d17a590c091e Mon Sep 17 00:00:00 2001 From: Bhscer Date: Thu, 17 Jul 2025 13:55:03 +0800 Subject: [PATCH 06/27] fix --- packages/ui-default/templates/contest_balloon.html | 2 +- packages/ui-default/templates/homework_detail.html | 2 +- packages/ui-default/templates/record_main_tr.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui-default/templates/contest_balloon.html b/packages/ui-default/templates/contest_balloon.html index a151baf18f..04dadfea2a 100644 --- a/packages/ui-default/templates/contest_balloon.html +++ b/packages/ui-default/templates/contest_balloon.html @@ -13,7 +13,7 @@

{{ _('Balloon Status') }}

{{ noscript_note.render() }}
- {% if tdoc.problems|length===0 or not tdoc.problems[0].balloon %} + {% if tdoc.problems|length == 0 or not tdoc.problems[0].balloon %} {{ nothing.render('Please set the balloon color for each problem first.') }} {% else %} diff --git a/packages/ui-default/templates/homework_detail.html b/packages/ui-default/templates/homework_detail.html index 3983efae14..7b498ad35c 100644 --- a/packages/ui-default/templates/homework_detail.html +++ b/packages/ui-default/templates/homework_detail.html @@ -59,7 +59,7 @@

{{ _('Problem') }}

{% endif %} - + @@ -157,7 +152,7 @@ const ContestProblemEditor: React.FC = ({ problems: i className="textbox" value={problem.title || ''} onChange={(e) => handleChange(index, 'title', e.target.value)} - placeholder={i18n('Empty will use raw title')} + placeholder={i18n('(leave blank if none)')} /> {% endif %} - - + diff --git a/packages/ui-default/templates/homework_edit.html b/packages/ui-default/templates/homework_edit.html index 78a89173cf..951736b5a9 100644 --- a/packages/ui-default/templates/homework_edit.html +++ b/packages/ui-default/templates/homework_edit.html @@ -103,7 +103,7 @@ {% for p in problems %} - + diff --git a/packages/utils/lib/common.ts b/packages/utils/lib/common.ts index 5ac6604e83..85d300745c 100644 --- a/packages/utils/lib/common.ts +++ b/packages/utils/lib/common.ts @@ -246,4 +246,12 @@ export const getAlphabeticId = (() => { return (i: number) => cache[i] || (i < 0 ? '?' : f(i)); })(); -export const getContestProblemConfig = (pid, tdoc) => tdoc.problems[tdoc.pid2idx?.[pid]]; +export const getContestProblemConfig = (pid, tdoc) => { + const idx = tdoc.pid2idx?.[pid]; + const cp = tdoc.problems[idx]; + // create label if not exists here will simplify the logic on template + return { + ...cp, + label: cp.label || (idx === undefined ? '' : getAlphabeticId(idx)), + }; +}; From 2dc66084916b69ada5e81adc9e9359e894352816 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Thu, 17 Jul 2025 19:31:48 +0800 Subject: [PATCH 10/27] fix dep in ui-default --- packages/ui-default/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index af10f25e44..6a21f31c1c 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -20,6 +20,7 @@ "@fontsource/roboto-mono": "^5.2.6", "@fontsource/source-code-pro": "^5.2.6", "@fontsource/ubuntu-mono": "^5.2.6", + "@hello-pangea/dnd": "^18.0.1", "@hydrooj/utils": "workspace:^", "@sentry/browser": "^9.33.0", "@sentry/webpack-plugin": "^3.5.0", @@ -121,7 +122,6 @@ "webpackbar": "^7.0.0" }, "dependencies": { - "@hello-pangea/dnd": "^18.0.1", "@hydrooj/common": "workspace:^", "ansi_up": "^6.0.6", "cssfilter": "^0.0.11", From 6f455cf297b743229af15b2aabf9af98969c5ed7 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Thu, 17 Jul 2025 19:49:02 +0800 Subject: [PATCH 11/27] fix some missing label --- packages/onsite-toolkit/index.ts | 4 ++-- packages/ui-default/pages/contest_balloon.page.tsx | 5 +++-- packages/ui-default/templates/contest_manage.html | 2 +- packages/ui-default/templates/contest_problemlist.html | 2 +- packages/ui-default/templates/problem_detail.html | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/onsite-toolkit/index.ts b/packages/onsite-toolkit/index.ts index 98c409e9cd..3368286bb1 100644 --- a/packages/onsite-toolkit/index.ts +++ b/packages/onsite-toolkit/index.ts @@ -2,7 +2,7 @@ import moment from 'moment'; import { avatar, ContestModel, ContestNotEndedError, Context, db, findFileSync, - ForbiddenError, fs, ObjectId, parseTimeMS, PERM, ProblemConfig, ProblemModel, + ForbiddenError, fs, getAlphabeticId, ObjectId, parseTimeMS, PERM, ProblemConfig, ProblemModel, STATUS, STATUS_SHORT_TEXTS, STATUS_TEXTS, Time, UserModel, Zip, } from 'hydrooj'; import { ResolverInput } from './interface'; @@ -59,7 +59,7 @@ export function apply(ctx: Context) { name: tdoc.title, duration: Math.floor((new Date(tdoc.endAt).getTime() - new Date(tdoc.beginAt).getTime()) / 1000), frozen: Math.floor((new Date(tdoc.lockAt).getTime() - new Date(tdoc.beginAt).getTime()) / 1000), - problems: tdoc.problems.map((p) => ({ name: p.label, id: p.pid.toString() })), + problems: tdoc.problems.map((p, idx) => ({ name: p.label || getAlphabeticId(idx), id: p.pid.toString() })), teams: teams.map((t) => ({ id: t.uid.toString(), name: udict[t.uid].displayName || udict[t.uid].uname, diff --git a/packages/ui-default/pages/contest_balloon.page.tsx b/packages/ui-default/pages/contest_balloon.page.tsx index abbf6a5600..439afa641a 100644 --- a/packages/ui-default/pages/contest_balloon.page.tsx +++ b/packages/ui-default/pages/contest_balloon.page.tsx @@ -1,4 +1,5 @@ /* eslint-disable react-refresh/only-export-components */ +import { getAlphabeticId } from '@hydrooj/utils/lib/common'; import yaml from 'js-yaml'; import React from 'react'; import { HexColorInput, HexColorPicker } from 'react-colorful'; @@ -32,8 +33,8 @@ function Balloon({ tdoc, val }) {
{% if isAdmin and not ntdoc %} - {{ problem.render_problem_title(pdict[pid], show_invisible_flag=false, show_tags=false, alphabetic=true) }} + {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false, alphabetic=false) }} {% else %} {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false, alphabetic=true) }} {% endif %} diff --git a/packages/ui-default/templates/record_main_tr.html b/packages/ui-default/templates/record_main_tr.html index 6ebd7f5726..5f9aa6c04f 100644 --- a/packages/ui-default/templates/record_main_tr.html +++ b/packages/ui-default/templates/record_main_tr.html @@ -20,7 +20,7 @@ {% if tdoc %} {{ problem.render_problem_title(pdoc, tdoc=tdoc, show_tags=false, show_invisible_flag=false, alphabetic=true) }} {% elif pdoc and rdoc.contest %} - {{ problem.render_problem_title(pdoc, tdoc=tdoc, show_tags=false, show_invisible_flag=false) }} + {{ problem.render_problem_title(pdoc, show_tags=false, show_invisible_flag=false) }} {% elif pdoc and (not pdoc.hidden or handler.user.hasPerm(perm.PERM_VIEW_PROBLEM_HIDDEN) or handler.user.own(pdoc)) %} {{ problem.render_problem_title(pdoc, show_tags=false) }} {% else %} From 2a15f6b3b61cc6b1f93b0bb3db1b8256447202cf Mon Sep 17 00:00:00 2001 From: Bhscer Date: Thu, 17 Jul 2025 16:05:20 +0800 Subject: [PATCH 07/27] fix --- packages/hydrooj/locales/zh.yaml | 2 +- packages/hydrooj/src/interface.ts | 1 + .../ContestProblemEditor.tsx | 45 +++++++++---------- .../templates/partials/contest_balloon.html | 5 ++- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index 99d6f99217..a1cc8904dc 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -61,6 +61,7 @@ Active Sessions: 活动会话 Add blacklist by ip, uid: 封禁 IP / 将对应 uid 的用户拉入黑名单 Add module: 添加模块 Add new data: 添加新数据 +Add Problem: 添加问题 Add User: 添加用户 Add: 添加 Additional File: 附加文件 @@ -321,7 +322,6 @@ Edit training plans: 修改训练计划 Edit: 编辑 Email Visibility: Email 可见性 Email: 电子邮件 -Empty will use raw title: 留空以使用原标题 Enabled: 开启 End at: 结束于 End Date: 结束日期 diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 43a999c641..399234a618 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -253,6 +253,7 @@ export interface ContestProblem { title?: string; score?: number; balloon?: { color: string, name: string }; + [key: string]: any, } export interface Tdoc extends Document { diff --git a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx index 6012570dd6..649b5b459a 100644 --- a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx +++ b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx @@ -1,5 +1,6 @@ import { getAlphabeticId } from '@hydrooj/utils/lib/common'; import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; +import { debounce } from 'lodash'; import React from 'react'; import { api, i18n } from 'vj/utils'; import ProblemSelectAutoComplete from '../autocomplete/components/ProblemSelectAutoComplete'; @@ -13,7 +14,7 @@ export interface Problem { color: string; name: string; }; - _tmpId?: string; // use this as key for better rander + _tmpId?: string; // use this as key } export interface ContestProblemEditorProps { @@ -21,14 +22,14 @@ export interface ContestProblemEditorProps { onChange: (problems: Problem[]) => void; } const randomId = () => Math.random().toString(16).substring(2); -const ContestProblemEditor: React.FC = ({ problems: initialProblems, onChange }) => { +const ContestProblemEditor: React.FC = ({ problems: initialProblems, onChange: _onChange }) => { // TODO: also support balloon and other fields in the future const [problems, setProblems] = React.useState(initialProblems.map((el) => ({ ...el, _tmpId: randomId() }))); const problemRefs = React.useRef<{ [key: number]: any }>({}); const [problemRawTitles, setProblemRawTitles] = React.useState>({}); - const fetchProblemTitles = async (ids: number[]) => { + const fetchProblemTitles = debounce(async (ids: number[]) => { api('problems', { ids }, ['docId', 'pid', 'title']) .then((res) => { setProblemRawTitles(res.reduce((acc, cur) => ({ ...acc, [cur.docId]: cur.title }), {})); @@ -36,13 +37,13 @@ const ContestProblemEditor: React.FC = ({ problems: i .catch(() => { // pid maybe not exist }); - }; + }, 500); React.useEffect(() => { fetchProblemTitles(problems.map((i) => i.pid).filter((i) => i)); }, []); - const beforeOnChange = (newProblems: Problem[]) => { + const onChange = (newProblems: Problem[]) => { const fixedProblems = newProblems.map((i) => { const problem = { ...i }; if (problem.title === '') delete problem.title; @@ -50,9 +51,8 @@ const ContestProblemEditor: React.FC = ({ problems: i return problem; }); setProblems(fixedProblems); - onChange(fixedProblems.map((i) => { - const p = { ...i }; - delete p._tmpId; + _onChange(fixedProblems.map((i) => { + const { _tmpId, ...p } = i; return p; })); }; @@ -60,13 +60,13 @@ const ContestProblemEditor: React.FC = ({ problems: i const handleAdd = () => { const newProblems = [...problems, { pid: 0, label: getAlphabeticId(problems.length), _tmpId: randomId() }]; setProblems(newProblems); - beforeOnChange(newProblems); + onChange(newProblems); }; const handleRemove = (index: number) => { const newProblems = problems.filter((_, i) => i !== index); setProblems(newProblems); - beforeOnChange(newProblems); + onChange(newProblems); }; const handleChange = (index: number, field: keyof Problem, value: string | number) => { @@ -86,26 +86,21 @@ const ContestProblemEditor: React.FC = ({ problems: i if (field === 'pid') { fetchProblemTitles(newProblems.map((i) => i.pid).filter((i) => i)); } - beforeOnChange(newProblems); + onChange(newProblems); }; const onDragEnd = (result) => { - console.log(result); if (!result.destination) return; - const newProblems = Array.from(problems); - // exchange label - [ - newProblems[result.source.index], newProblems[result.destination.index], - ] = [ - newProblems[result.destination.index], newProblems[result.source.index], - ]; - const [labelX, labelY] = [newProblems[result.source.index].label, newProblems[result.destination.index].label]; - newProblems[result.source.index].label = labelY; - newProblems[result.destination.index].label = labelX; + const newProblems = [...problems]; + const [iX, iY] = [result.source.index, result.destination.index]; + [newProblems[iX], newProblems[iY]] = [newProblems[iY], newProblems[iX]]; + const [labelX, labelY] = [newProblems[iX].label, newProblems[iY].label]; + newProblems[iX].label = labelY; + newProblems[iY].label = labelX; setProblems(newProblems); - beforeOnChange(newProblems); + onChange(newProblems); }; return ( @@ -115,7 +110,7 @@ const ContestProblemEditor: React.FC = ({ problems: i
Pidpid {i18n('Raw Title')} {i18n('Custom Title')} {i18n('Label')} diff --git a/packages/ui-default/templates/partials/contest_balloon.html b/packages/ui-default/templates/partials/contest_balloon.html index b30d89f90d..36dd1d69fd 100644 --- a/packages/ui-default/templates/partials/contest_balloon.html +++ b/packages/ui-default/templates/partials/contest_balloon.html @@ -15,7 +15,8 @@ {{ bdoc._id.toHexString()|truncate(8,true,'') }} + {% set cp = utils.getContestProblemConfig(bdoc.pid, tdoc) %} + {% if not bdoc.sent %}
@@ -25,7 +26,7 @@
{% endif %} - {{ utils.getContestProblemConfig(bdoc.pid, tdoc).label }}  ({{ tdoc.balloon[bdoc.pid].name }}) + {{ cp.label }}  ({{ cp.balloon.name }})
{{ user.render_inline(udict[bdoc.uid], badge=false) }} From c5ab8cc88ce575a7856a36b9f21fd9d88a60ecf7 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Thu, 17 Jul 2025 17:11:29 +0800 Subject: [PATCH 08/27] optimze --- .../contestProblemEditor/ContestProblemEditor.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx index 649b5b459a..7c97bff5e6 100644 --- a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx +++ b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx @@ -32,7 +32,10 @@ const ContestProblemEditor: React.FC = ({ problems: i const fetchProblemTitles = debounce(async (ids: number[]) => { api('problems', { ids }, ['docId', 'pid', 'title']) .then((res) => { - setProblemRawTitles(res.reduce((acc, cur) => ({ ...acc, [cur.docId]: cur.title }), {})); + setProblemRawTitles(res.reduce((acc, cur) => { + acc[cur.docId] = cur.title; + return acc; + }, {})); }) .catch(() => { // pid maybe not exist @@ -46,8 +49,9 @@ const ContestProblemEditor: React.FC = ({ problems: i const onChange = (newProblems: Problem[]) => { const fixedProblems = newProblems.map((i) => { const problem = { ...i }; - if (problem.title === '') delete problem.title; - if (problem.score === 100) delete problem.score; + // undefined is ok, JSON.stringify will ignore it + if (problem.title === '') problem.title = undefined; + if (problem.score === 100) problem.score = undefined; return problem; }); setProblems(fixedProblems); From 2ff5a0160646db62a0b6687d78949fb57bad8e21 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Thu, 17 Jul 2025 19:27:20 +0800 Subject: [PATCH 09/27] fix --- packages/hydrooj/src/handler/record.ts | 8 ++- packages/hydrooj/src/interface.ts | 6 +- packages/hydrooj/src/model/contest.ts | 59 +++++++++---------- packages/hydrooj/src/upgrade.ts | 5 -- .../ContestProblemEditor.tsx | 19 ++++-- ...est_edit.page.ts => contest_edit.page.tsx} | 17 +++--- .../ui-default/templates/contest_edit.html | 2 +- .../ui-default/templates/homework_edit.html | 2 +- packages/utils/lib/common.ts | 10 +++- 9 files changed, 68 insertions(+), 60 deletions(-) rename packages/ui-default/pages/{contest_edit.page.ts => contest_edit.page.tsx} (88%) diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index ec4fc1e64c..905d7d6e82 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -20,7 +20,7 @@ import user from '../model/user'; import { ConnectionHandler, param, subscribe, Types, } from '../service/server'; -import { buildProjection, Time } from '../utils'; +import { buildProjection, getAlphabeticId, Time } from '../utils'; import { ContestDetailBaseHandler } from './contest'; import { postJudge } from './judge'; @@ -72,7 +72,10 @@ class RecordListHandler extends ContestDetailBaseHandler { const realPid = pid; if (pid) { if (typeof pid === 'string' && tdoc) { - const result = tdoc.problems.find((i) => i.label === pid); + const result = tdoc.problems.find((i, idx) => { + if (i.label) return i.label === pid; + return pid === getAlphabeticId(idx); + }); if (result) pid = result.pid; } const pdoc = await problem.get(domainId, pid); @@ -304,6 +307,7 @@ class RecordMainConnectionHandler extends ConnectionHandler { if (pid) { const pdoc = await problem.get(domainId, pid); if (pdoc) this.pid = pdoc.docId; + // FIXME: error will be throw if pid is problem's label in contest else throw new ProblemNotFoundError(domainId, pid); } if (status) this.status = status; diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 399234a618..67ac28f7c7 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -249,7 +249,7 @@ export interface TrainingNode { export interface ContestProblem { pid: number; - label: string; + label?: string; title?: string; score?: number; balloon?: { color: string, name: string }; @@ -267,7 +267,6 @@ export interface Tdoc extends Document { rule: string; pids: number[]; problems: ContestProblem[]; - pid2idx: Record; rated?: boolean; _code?: string; assign?: string[]; @@ -295,6 +294,9 @@ export interface Tdoc extends Document { // For training description?: string; dag?: TrainingNode[]; + + // not stored in database + pid2idx?: Record; } export interface TrainingDoc extends Omit { diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index ebc4b37bcd..95437e3b59 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -162,7 +162,7 @@ const acm = buildContestRule({ } else { columns.push({ type: 'problem', - value: cp.label, + value: cp.label || getAlphabeticId(i - 1), raw: pid, }); } @@ -330,7 +330,7 @@ const oi = buildContestRule({ } else { columns.push({ type: 'problem', - value: cp.label, + value: cp.label || getAlphabeticId(i - 1), raw: cp.pid, }); } @@ -355,10 +355,8 @@ const oi = buildContestRule({ row.push({ type: 'total_score', value: tsdoc.score || 0 }); const accepted = {}; for (const s of tsdoc.journal || []) { - // console.log(typeof s.pid, s.pid, !!pdict[s.pid]); if (!pdict[s.pid]) continue; if (config.lockAt && s.rid.getTimestamp() > config.lockAt) continue; - // console.log(s.pid, pdict[s.pid].nSubmit) pdict[s.pid].nSubmit++; if (s.status === STATUS.STATUS_ACCEPTED && !accepted[s.pid]) { pdict[s.pid].nAccept++; @@ -715,7 +713,7 @@ const homework = buildContestRule({ } else { columns.push({ type: 'problem', - value: cp.label, + value: cp.label || getAlphabeticId(i - 1), raw: pid, }); } @@ -822,17 +820,13 @@ export async function add( ...(data?.score && data.score[pid] ? { score: data.score[pid] } : {}), })); } - const pid2idx = {}; - for (let i = 0; i < problems.length; i++) { - pid2idx[problems[i].pid] = i; - } Object.assign(data, { - content, owner, title, rule, beginAt, endAt, pids, problems, pid2idx, attend: 0, + content, owner, title, rule, beginAt, endAt, pids, problems, attend: 0, }); RULES[rule].check(data); await bus.parallel('contest/before-add', data); const docId = await document.add(domainId, content, owner, document.TYPE_CONTEST, null, null, null, { - assign: [], ...data, title, rule, beginAt, endAt, pids, problems, pid2idx, attend: 0, rated, + assign: [], ...data, title, rule, beginAt, endAt, pids, problems, attend: 0, rated, }); await bus.parallel('contest/add', data, docId); return docId; @@ -884,12 +878,6 @@ export async function edit(domainId: string, tid: ObjectId, $set: Partial) } : {}), })); } - if ($set.problems) { - $set.pid2idx = {}; - for (let i = 0; i < $set.problems.length; i++) { - $set.pid2idx[$set.problems[i].pid] = i; - } - } const res = await document.set(domainId, document.TYPE_CONTEST, tid, $set); await bus.parallel('contest/edit', res); return res; @@ -906,7 +894,13 @@ export async function del(domainId: string, tid: ObjectId) { export async function get(domainId: string, tid: ObjectId): Promise { const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); if (!tdoc) throw new ContestNotFoundError(tid); - return tdoc; + return { + ...tdoc, + pid2idx: tdoc.problems.reduce((acc, cur, idx) => { + acc[cur.pid] = idx; + return acc; + }, {}), + }; } export async function getRelated(domainId: string, pid: number, rule?: string) { @@ -1122,18 +1116,17 @@ export const statusText = (tdoc: Tdoc, tsdoc?: any) => ( ? 'Live...' : 'Done'); -export function resolveContestProblemJson(text:string) { - const validate = Schema.array(Schema.object({ - pid: Schema.number().required(), - label: Schema.string().required(), - title: Schema.string(), - score: Schema.number().min(0), - balloon: Schema.object({ - color: Schema.string(), - name: Schema.string(), - }), - })).required(); - +export const ContestProblemJsonSchema = Schema.array(Schema.object({ + pid: Schema.number().required(), + label: Schema.string(), + title: Schema.string(), + score: Schema.number().min(0), + balloon: Schema.object({ + color: Schema.string(), + name: Schema.string(), + }), +})).required(); +export function resolveContestProblemJson(text:string, validate = ContestProblemJsonSchema) { let _problemList = []; try { _problemList = validate(JSON.parse(text)); @@ -1142,15 +1135,16 @@ export function resolveContestProblemJson(text:string) { } const problems = [] as ContestProblem[]; const score = {} as Record; - for (const p of _problemList) { + for (let i = 0; i < _problemList.length; i++) { + const p = _problemList[i]; if (typeof p.score === 'number' && p.score !== 100) { score[p.pid] = p.score; } problems.push({ pid: p.pid, - label: p.label, ...(p.score && p.score !== 100 ? { score: p.score } : {}), ...(p.title && p.title.length > 0 ? { title: p.title } : {}), + ...(p.label && p.label.length > 0 && p.label !== getAlphabeticId(i) ? { label: p.label } : {}), }); } return { @@ -1202,4 +1196,5 @@ global.Hydro.model.contest = { applyProjection, statusText, resolveContestProblemJson, + ContestProblemJsonSchema, }; diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 0f8a728c61..8b94ad8a93 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -622,10 +622,6 @@ export const coreScripts: MigrationScript[] = [ logger.info('Processing domain %s', _id); const tdocs = await contest.getMulti(_id, {}).toArray(); for (const tdoc of tdocs) { - const pid2idx = {}; - for (let i = 0; i < tdoc.pids.length; i++) { - pid2idx[tdoc.pids[i]] = i; - } await contest.edit(_id, tdoc._id, { problems: tdoc.pids.map((pid, idx) => ({ pid, @@ -638,7 +634,6 @@ export const coreScripts: MigrationScript[] = [ }, } : {}), })), - pid2idx, }); } logger.info('Domain %s done', _id); diff --git a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx index 7c97bff5e6..0e4a3ea27f 100644 --- a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx +++ b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx @@ -7,7 +7,7 @@ import ProblemSelectAutoComplete from '../autocomplete/components/ProblemSelectA export interface Problem { pid: number; - label: string; + label?: string; title?: string; score?: number; balloon?: { @@ -22,9 +22,13 @@ export interface ContestProblemEditorProps { onChange: (problems: Problem[]) => void; } const randomId = () => Math.random().toString(16).substring(2); -const ContestProblemEditor: React.FC = ({ problems: initialProblems, onChange: _onChange }) => { +const ContestProblemEditor = ({ problems: initialProblems, onChange: _onChange } : ContestProblemEditorProps) => { // TODO: also support balloon and other fields in the future - const [problems, setProblems] = React.useState(initialProblems.map((el) => ({ ...el, _tmpId: randomId() }))); + const [problems, setProblems] = React.useState(initialProblems.map((el, idx) => ({ + ...el, + _tmpId: randomId(), + ...(!el.label ? { label: getAlphabeticId(idx) } : {}), + }))); const problemRefs = React.useRef<{ [key: number]: any }>({}); const [problemRawTitles, setProblemRawTitles] = React.useState>({}); @@ -55,9 +59,12 @@ const ContestProblemEditor: React.FC = ({ problems: i return problem; }); setProblems(fixedProblems); - _onChange(fixedProblems.map((i) => { - const { _tmpId, ...p } = i; - return p; + _onChange(fixedProblems.map((i, idx) => { + const { _tmpId, label, ...p } = i; + return { + ...p, + ...(label !== getAlphabeticId(idx) ? { label } : {}), + }; })); }; diff --git a/packages/ui-default/pages/contest_edit.page.ts b/packages/ui-default/pages/contest_edit.page.tsx similarity index 88% rename from packages/ui-default/pages/contest_edit.page.ts rename to packages/ui-default/pages/contest_edit.page.tsx index 17bea54876..a86c0821c1 100644 --- a/packages/ui-default/pages/contest_edit.page.ts +++ b/packages/ui-default/pages/contest_edit.page.tsx @@ -10,20 +10,19 @@ import { ConfirmDialog } from 'vj/components/dialog'; import { NamedPage } from 'vj/misc/Page'; import { i18n, request, tpl } from 'vj/utils'; -const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', 'homework_edit'], (pagename) => { +export default new NamedPage(['contest_edit', 'contest_create', 'homework_create', 'homework_edit'], (pagename) => { ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true, clearDefaultValue: false }); UserSelectAutoComplete.getOrConstruct($('[name="maintainer"]'), { multi: true, clearDefaultValue: false }); LanguageSelectAutoComplete.getOrConstruct($('[name=langs]'), { multi: true }); if ($('#problem-editor').length) { - const problemEditor = $('#problem-editor'); const problemsInput = $('[name=problems]'); - ReactDOM.createRoot(problemEditor[0]).render( - React.createElement(ContestProblemEditor, { - onChange: (problems) => { + ReactDOM.createRoot($('#problem-editor')[0]).render( + { problemsInput.val(JSON.stringify(problems)); - }, - problems: JSON.parse(problemsInput.val() as string), - }), + }} + />, ); } $('[name="rule"]').on('change', () => { @@ -74,5 +73,3 @@ const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', }, 500); } }); - -export default page; diff --git a/packages/ui-default/templates/contest_edit.html b/packages/ui-default/templates/contest_edit.html index abdf99be6b..19f97c80cc 100644 --- a/packages/ui-default/templates/contest_edit.html +++ b/packages/ui-default/templates/contest_edit.html @@ -82,7 +82,7 @@

{{ _('Basic Info') }}

{% for p in problems %}
{{ p.pid }}{{ p.label }}{{ p.label|default(utils.getAlphabeticId(tdoc.pid2idx[p.pid])) }} {{ p.score|default(100) }} {{ p.title }}
{{ p.pid }}{{ p.label }}{{ p.label|default(utils.getAlphabeticId(tdoc.pid2idx[p.pid])) }} {{ p.score|default(100) }} {{ p.title }}
{now === pid - ? ({cp.label}) - : ({cp.label})} + ? ({cp.label || getAlphabeticId(idx)}) + : ({cp.label || getAlphabeticId(idx)})} {{ _('Send Broadcast Message') }} {% for cp in tdoc.problems %} - + {% endfor %} diff --git a/packages/ui-default/templates/contest_problemlist.html b/packages/ui-default/templates/contest_problemlist.html index c1bd729e59..4503445457 100644 --- a/packages/ui-default/templates/contest_problemlist.html +++ b/packages/ui-default/templates/contest_problemlist.html @@ -186,7 +186,7 @@

{{ _('Send Clarification Request') }}

{% for cp in tdoc.problems %} - + {% endfor %} diff --git a/packages/ui-default/templates/problem_detail.html b/packages/ui-default/templates/problem_detail.html index 7cda284b65..a7e1bd37a1 100644 --- a/packages/ui-default/templates/problem_detail.html +++ b/packages/ui-default/templates/problem_detail.html @@ -81,7 +81,7 @@

- {{ cp.label }} + {{ cp.label|default(utils.getAlphabeticId(tdoc.pid2idx[cp.pid])) }} {% if status %}{% endif %} {%- endfor %} From cce8108100820240b796f9d7107d60a99b54b792 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Thu, 17 Jul 2025 21:56:21 +0800 Subject: [PATCH 12/27] label is no need when upgrade --- packages/hydrooj/src/upgrade.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 8b94ad8a93..3c32c6532d 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -625,7 +625,6 @@ export const coreScripts: MigrationScript[] = [ await contest.edit(_id, tdoc._id, { problems: tdoc.pids.map((pid, idx) => ({ pid, - label: getAlphabeticId(idx), ...(tdoc?.score && tdoc.score[pid] ? { score: tdoc.score[pid] } : {}), ...(tdoc?.balloon && tdoc.balloon[pid] ? { balloon: { From 8d661b5de3da13e245c2867f97cd749bcb7d61fd Mon Sep 17 00:00:00 2001 From: Bhscer Date: Thu, 17 Jul 2025 22:40:49 +0800 Subject: [PATCH 13/27] lagacy add/edit contest pids need no label --- packages/hydrooj/src/model/contest.ts | 10 ++++------ packages/hydrooj/src/upgrade.ts | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 95437e3b59..842a5f34b5 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -812,12 +812,11 @@ export async function add( if (!RULES[rule]) throw new ValidationError('rule'); if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt'); // TODO: this is the best way to support old plugins, but need remove one day - let problems = data?.problems || []; + let problems = data.problems || []; if (problems.length === 0 && pids.length > 0) { - problems = pids.map((pid, idx) => ({ + problems = pids.map((pid) => ({ pid, - label: getAlphabeticId(idx), - ...(data?.score && data.score[pid] ? { score: data.score[pid] } : {}), + ...(data.score?.[pid] ? { score: data.score[pid] } : {}), })); } Object.assign(data, { @@ -847,9 +846,8 @@ export async function edit(domainId: string, tid: ObjectId, $set: Partial) }, {})), ...($set.score ? $set.score : {}), }; - $set.problems = $set.pids.map((pid, idx) => ({ + $set.problems = $set.pids.map((pid) => ({ pid, - label: getAlphabeticId(idx), ...(mergedScore[pid] ? { score: mergedScore[pid] } : {}), ...($set.balloon ? ( diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 3c32c6532d..0eb1e7ad5f 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import yaml from 'js-yaml'; import { ObjectId } from 'mongodb'; -import { getAlphabeticId, randomstring, sleep } from '@hydrooj/utils'; +import { randomstring, sleep } from '@hydrooj/utils'; import { buildContent } from './lib/content'; import { Logger } from './logger'; import { PERM, PRIV, STATUS } from './model/builtin'; From c9772e3c7a0ab18d968ac9bb1706e801179d5e71 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Fri, 18 Jul 2025 11:15:59 +0800 Subject: [PATCH 14/27] add deprecated flag && fix --- packages/hydrooj/src/handler/homework.ts | 6 +++--- packages/hydrooj/src/interface.ts | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/hydrooj/src/handler/homework.ts b/packages/hydrooj/src/handler/homework.ts index 69258f2d62..af657002b7 100644 --- a/packages/hydrooj/src/handler/homework.ts +++ b/packages/hydrooj/src/handler/homework.ts @@ -2,7 +2,7 @@ import yaml from 'js-yaml'; import { escapeRegExp, pick } from 'lodash'; import moment from 'moment-timezone'; import { ObjectId } from 'mongodb'; -import { sortFiles, Time } from '@hydrooj/utils/lib/utils'; +import { diffArray, sortFiles, Time } from '@hydrooj/utils/lib/utils'; import { ContestNotFoundError, FileLimitExceededError, FileUploadError, HomeworkNotLiveError, NotAssignedError, ValidationError, } from '../error'; @@ -116,7 +116,7 @@ class HomeworkDetailHandler extends Handler { && !this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD) ) return; - const pdict = await problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST); + const pdict = await problem.getList(domainId, this.tdoc.problems.map((p) => p.pid), true, true, problem.PROJECTION_CONTEST_LIST); const psdict = {}; let rdict = {}; if (tsdoc) { @@ -237,7 +237,7 @@ class HomeworkEditHandler extends Handler { if (tdoc.beginAt !== beginAt.toDate() || tdoc.endAt !== endAt.toDate() || tdoc.penaltySince !== penaltySince.toDate() - || tdoc.pids.sort().join(' ') !== pids.sort().join(' ')) { + || diffArray(tdoc.problems.map((i) => i.pid), pids)) { await contest.recalcStatus(domainId, tdoc.docId); } } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 67ac28f7c7..99958a040e 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -265,6 +265,7 @@ export interface Tdoc extends Document { title: string; content: string; rule: string; + /** @deprecated */ pids: number[]; problems: ContestProblem[]; rated?: boolean; @@ -277,7 +278,9 @@ export interface Tdoc extends Document { lockAt?: Date; unlocked?: boolean; autoHide?: boolean; + /** @deprecated */ balloon?: Record; + /** @deprecated */ score?: Record; langs?: string[]; From 021e0b9b3bbb33fc30861e22f29526747a3d297d Mon Sep 17 00:00:00 2001 From: Bhscer Date: Fri, 18 Jul 2025 11:39:21 +0800 Subject: [PATCH 15/27] admin view homework problem need no tid param --- packages/ui-default/templates/components/problem.html | 4 ++-- packages/ui-default/templates/homework_detail.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui-default/templates/components/problem.html b/packages/ui-default/templates/components/problem.html index 471828a02d..857bf59d03 100644 --- a/packages/ui-default/templates/components/problem.html +++ b/packages/ui-default/templates/components/problem.html @@ -1,7 +1,7 @@ -{% macro render_problem_title(pdoc, tdoc=none, show_tags=true, show_invisible_flag=true, invalid=false, inline=false, show_pid=true, small=false, alphabetic=false) %} +{% macro render_problem_title(pdoc, tdoc=none, show_tags=true, show_invisible_flag=true, invalid=false, inline=false, show_pid=true, small=false, alphabetic=false, url_with_contest=true) %} {%- if not invalid -%} {% set _linkArgs = { pid:pdoc.pid|default(pdoc.docId) } %} - {% if tdoc %}{{ set(_linkArgs, 'query', {tid:tdoc.docId}) }}{% endif %} + {% if tdoc and url_with_contest %}{{ set(_linkArgs, 'query', {tid:tdoc.docId}) }}{% endif %} {%- endif -%} {% if show_pid %} diff --git a/packages/ui-default/templates/homework_detail.html b/packages/ui-default/templates/homework_detail.html index 7b498ad35c..af65217f82 100644 --- a/packages/ui-default/templates/homework_detail.html +++ b/packages/ui-default/templates/homework_detail.html @@ -59,7 +59,7 @@

{{ _('Problem') }}

{% endif %}

{% if isAdmin and not ntdoc %} - {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false, alphabetic=false) }} + {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false, alphabetic=false, url_with_contest=false) }} {% else %} {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false, alphabetic=true) }} {% endif %} From 5262ee3a42ca4c102d90b6c5955d115065d39f9b Mon Sep 17 00:00:00 2001 From: Bhscer Date: Fri, 18 Jul 2025 12:04:16 +0800 Subject: [PATCH 16/27] support restore problem when back from the ValidationError page --- packages/ui-default/pages/contest_edit.page.tsx | 1 - packages/ui-default/templates/contest_edit.html | 2 +- packages/ui-default/templates/homework_detail.html | 2 +- packages/ui-default/templates/homework_edit.html | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/ui-default/pages/contest_edit.page.tsx b/packages/ui-default/pages/contest_edit.page.tsx index a86c0821c1..8255d0a4c4 100644 --- a/packages/ui-default/pages/contest_edit.page.tsx +++ b/packages/ui-default/pages/contest_edit.page.tsx @@ -1,6 +1,5 @@ import $ from 'jquery'; import moment from 'moment'; -import React from 'react'; import ReactDOM from 'react-dom/client'; import LanguageSelectAutoComplete from 'vj/components/autocomplete/LanguageSelectAutoComplete'; import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete'; diff --git a/packages/ui-default/templates/contest_edit.html b/packages/ui-default/templates/contest_edit.html index 19f97c80cc..5bdc1f3ca0 100644 --- a/packages/ui-default/templates/contest_edit.html +++ b/packages/ui-default/templates/contest_edit.html @@ -67,7 +67,7 @@

{{ _('Basic Info') }}

- +
diff --git a/packages/ui-default/templates/homework_detail.html b/packages/ui-default/templates/homework_detail.html index af65217f82..f34b8f4be6 100644 --- a/packages/ui-default/templates/homework_detail.html +++ b/packages/ui-default/templates/homework_detail.html @@ -59,7 +59,7 @@

{{ _('Problem') }}

{% endif %} drag(drop(node))} style={{ opacity: isDragging ? 0.5 : 1 }}> - + + - - - + diff --git a/packages/ui-default/pages/contest_balloon.page.tsx b/packages/ui-default/pages/contest_balloon.page.tsx index 439afa641a..975bed8a71 100644 --- a/packages/ui-default/pages/contest_balloon.page.tsx +++ b/packages/ui-default/pages/contest_balloon.page.tsx @@ -26,15 +26,15 @@ function Balloon({ tdoc, val }) { - {tdoc.problems.map((cp, idx) => { - const pid = cp.pid; + {tdoc.pids.map((pid, idx) => { + const cp = tdoc.problemConfig[pid]; const { color: c, name } = val[+pid]; return (
{% if isAdmin and not ntdoc %} - {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false, alphabetic=false, url_with_contest=false) }} + {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false, alphabetic=true, url_with_contest=false) }} {% else %} {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false, alphabetic=true) }} {% endif %} diff --git a/packages/ui-default/templates/homework_edit.html b/packages/ui-default/templates/homework_edit.html index 951736b5a9..602a484e01 100644 --- a/packages/ui-default/templates/homework_edit.html +++ b/packages/ui-default/templates/homework_edit.html @@ -88,7 +88,7 @@
- +
From 956c4b82db8f9ceb256f54d0b95d5b49d9db9dd1 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Fri, 18 Jul 2025 20:13:32 +0800 Subject: [PATCH 17/27] fix recalcStatus missing && optimize the scoreboard with large data --- packages/hydrooj/src/handler/contest.ts | 3 ++- packages/hydrooj/src/model/contest.ts | 2 +- packages/ui-default/templates/partials/scoreboard.html | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 08eccf3580..ba84db4936 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -787,9 +787,10 @@ export async function apply(ctx: Context) { const page_name = tdoc.rule === 'homework' ? 'homework_scoreboard' : 'contest_scoreboard'; + const isLargeBoard = rows.length * Object.keys(udict).length > 10000; // > 16 problem * 500 user const availableViews = scoreboard.getAvailableViews(tdoc.rule); this.response.body = { - tdoc: this.tdoc, tsdoc: this.tsdocAsPublic(), rows, udict, pdict, page_name, groups, availableViews, + tdoc: this.tdoc, tsdoc: this.tsdocAsPublic(), rows, udict, pdict, page_name, groups, availableViews, isLargeBoard, }; this.response.pjax = 'partials/scoreboard.html'; this.response.template = 'contest_scoreboard.html'; diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 842a5f34b5..0a9c064afc 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -1004,7 +1004,7 @@ export async function getAndListStatus(domainId: string, tid: ObjectId): Promise export async function recalcStatus(domainId: string, tid: ObjectId) { const [tdoc, tsdocs] = await Promise.all([ - document.get(domainId, document.TYPE_CONTEST, tid), + get(domainId, tid), document.getMultiStatus(domainId, document.TYPE_CONTEST, { docId: tid }).toArray(), ]); const tasks = []; diff --git a/packages/ui-default/templates/partials/scoreboard.html b/packages/ui-default/templates/partials/scoreboard.html index 7f3343f54a..f171d3a0f2 100644 --- a/packages/ui-default/templates/partials/scoreboard.html +++ b/packages/ui-default/templates/partials/scoreboard.html @@ -2,7 +2,7 @@ {% macro renderRecord(cell, canView) %} {% if canView %}{% endif %} {%- set _color = utils.status.getScoreColor(cell.score|default(cell.value)) -%} - {{ cell.value|string|nl2br|safe }} + {{ cell.value|string|nl2br|safe }} {% if canView %}{% endif %} {% endmacro %} @@ -67,7 +67,7 @@ {%- endif -%} {%- endfor -%} {%- else -%} - {{ column.value|string|nl2br|safe }} + {{ column.value|string|nl2br|safe }} {%- endif -%} {%- endfor -%} From 5d55662eba9961feaed68dd94f58db58faab1137 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Fri, 18 Jul 2025 20:17:08 +0800 Subject: [PATCH 18/27] fix the judgement of a scoreboard is large or not --- packages/hydrooj/src/handler/contest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index ba84db4936..5ba60505ec 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -787,7 +787,7 @@ export async function apply(ctx: Context) { const page_name = tdoc.rule === 'homework' ? 'homework_scoreboard' : 'contest_scoreboard'; - const isLargeBoard = rows.length * Object.keys(udict).length > 10000; // > 16 problem * 500 user + const isLargeBoard = rows.length * Object.keys(pdict).length > 10000; // > 16 problem * 500 user const availableViews = scoreboard.getAvailableViews(tdoc.rule); this.response.body = { tdoc: this.tdoc, tsdoc: this.tsdocAsPublic(), rows, udict, pdict, page_name, groups, availableViews, isLargeBoard, From 418b4be7e1f4b4513f912b1cc2f15df360f08020 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Fri, 18 Jul 2025 20:24:54 +0800 Subject: [PATCH 19/27] fix a missing judge of tooltip --- packages/ui-default/templates/partials/scoreboard.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui-default/templates/partials/scoreboard.html b/packages/ui-default/templates/partials/scoreboard.html index f171d3a0f2..c8ae4e1d3f 100644 --- a/packages/ui-default/templates/partials/scoreboard.html +++ b/packages/ui-default/templates/partials/scoreboard.html @@ -51,8 +51,8 @@ {%- elif column.type == 'user' -%} {%- set canView = canViewAll or handler.user._id == column.raw -%} {{ user.render_inline(udict[column.raw], badge=false) }} {%- elif column.type == 'record' and column.raw -%} From d63c2a98384a73487d69663a7a1d5d73e04e37ff Mon Sep 17 00:00:00 2001 From: Bhscer Date: Fri, 18 Jul 2025 21:01:31 +0800 Subject: [PATCH 20/27] fix some missing usage of pid2idx --- packages/hydrooj/src/handler/contest.ts | 6 +++--- packages/hydrooj/src/model/contest.ts | 8 ++++---- packages/onsite-toolkit/index.ts | 4 ++-- packages/scoreboard-xcpcio/index.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 5ba60505ec..6ec7b46ea9 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -249,7 +249,7 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler { if (!this.user.own(this.tdoc)) { await message.send(1, this.tdoc.maintainer.concat(this.tdoc.owner), JSON.stringify({ message: 'Contest {0} has a new clarification about {1}, please go to contest management to reply.', - params: [this.tdoc.title, subject > 0 ? `#${this.tdoc.problems.findIndex((p) => p.pid === subject) + 1}` : 'the contest'], + params: [this.tdoc.title, subject > 0 ? `#${this.tdoc.pid2idx[subject] + 1}` : 'the contest'], url: this.url('contest_manage', { tid }), }), message.FLAG_I18N | message.FLAG_UNREAD); } @@ -524,8 +524,8 @@ export class ContestManagementHandler extends ContestManagementBaseHandler { @param('pid', Types.PositiveInt) @param('score', Types.PositiveInt) async postSetScore(domainId: string, pid: number, score: number) { - const idx = this.tdoc.problems.findIndex((i) => i.pid === pid); - if (idx === -1) throw new ValidationError('pid'); + const idx = this.tdoc.pid2idx[pid]; + if (idx === undefined) throw new ValidationError('pid'); this.tdoc.problems[idx].score = score; // TODO: remove `score` field later this.tdoc.score = this.tdoc.problems.reduce( diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 0a9c064afc..be86deed9f 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -344,7 +344,7 @@ const oi = buildContestRule({ ]; const displayScore = (pid: number, score?: number) => { if (typeof score !== 'number') return '-'; - return score * ((tdoc.problems.find((p) => p.pid === pid)?.score || 100) / 100); + return score * ((tdoc.problems[tdoc.pid2idx[pid]]?.score || 100) / 100); }; if (config.isExport && config.showDisplayName) { row.push({ type: 'email', value: udoc.mail }); @@ -474,7 +474,7 @@ const strictioi = buildContestRule({ const detail = {}; let score = 0; const subtasks: Record> = {}; - for (const j of journal.filter((i) => tdoc.problems.find((p) => p.pid === i.pid))) { + for (const j of journal.filter((i) => Object.hasOwn(tdoc.pid2idx, i.pid))) { subtasks[j.pid] ||= {}; for (const i in j.subtasks) { if (!subtasks[j.pid][i] || subtasks[j.pid][i].score < j.subtasks[i].score) subtasks[j.pid][i] = j.subtasks[i]; @@ -557,7 +557,7 @@ const ledo = buildContestRule({ stat(tdoc, journal) { const ntry = Counter(); const detail = {}; - for (const j of journal.filter((i) => tdoc.problems.find((p) => p.pid === i.pid))) { + for (const j of journal.filter((i) => Object.hasOwn(tdoc.pid2idx, i.pid))) { const vaild = ![STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_FORMAT_ERROR].includes(j.status); if (vaild) ntry[j.pid]++; const penaltyScore = vaild ? Math.round(Math.max(0.7, 0.95 ** (ntry[j.pid] - 1)) * j.score) : 0; @@ -946,7 +946,7 @@ export async function updateStatus( }: { status?: STATUS, score?: number, subtasks?: Record, lang?: string } = {}, ) { const tdoc = await get(domainId, tid); - if (tdoc.problems.find((p) => p.pid === pid)?.balloon && status === STATUS.STATUS_ACCEPTED) await addBalloon(domainId, tid, uid, rid, pid); + if (tdoc.problems[tdoc.pid2idx[pid]]?.balloon && status === STATUS.STATUS_ACCEPTED) await addBalloon(domainId, tid, uid, rid, pid); const tsdoc = await document.revPushStatus(tdoc.domainId, document.TYPE_CONTEST, tdoc.docId, uid, 'journal', { rid, pid, status, score, subtasks, lang, }, 'rid'); diff --git a/packages/onsite-toolkit/index.ts b/packages/onsite-toolkit/index.ts index 3368286bb1..2f8c0300bc 100644 --- a/packages/onsite-toolkit/index.ts +++ b/packages/onsite-toolkit/index.ts @@ -52,7 +52,7 @@ export function apply(ctx: Context) { const unknownSchool = this.translate('Unknown School'); const submissions = teams.flatMap((i) => { if (!i.journal) return []; - return i.journal.filter((s) => tdoc.problems.find((p) => p.pid === s.pid)).map((s) => ({ ...s, uid: i.uid })); + return i.journal.filter((s) => Object.hasOwn(tdoc.pid2idx, s.pid)).map((s) => ({ ...s, uid: i.uid })); }); this.response.body = { payload: { @@ -206,7 +206,7 @@ export function apply(ctx: Context) { let cntJudge = 0; const submissions = tsdocs.flatMap((i) => { if (!i.journal) return []; - const journal = i.journal.filter((s) => tdoc.problems.find((p) => p.pid === s.pid)); + const journal = i.journal.filter((s) => Object.hasOwn(tdoc.pid2idx, s.pid)); const result: any[] = []; for (const s of journal) { const submitTime = moment(s.rid.getTimestamp()); diff --git a/packages/scoreboard-xcpcio/index.ts b/packages/scoreboard-xcpcio/index.ts index 7385e3d843..cfaf0b9f7e 100644 --- a/packages/scoreboard-xcpcio/index.ts +++ b/packages/scoreboard-xcpcio/index.ts @@ -103,7 +103,7 @@ export async function apply(ctx: Context) { const submit = new ObjectId(j.rid as string).getTimestamp().getTime(); const curStatus = status[j.status] || 'SYSTEM_ERROR'; return { - problem_id: tdoc.problems.findIndex((p) => p.pid === j.pid), + problem_id: tdoc.pid2idx[j.pid], team_id: `${i.uid}`, timestamp: Math.floor(submit - tdoc.beginAt.getTime()), status: realtime From 16cc94bd9ae42292347a8f1a847302a0e2255dd6 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Fri, 18 Jul 2025 23:01:47 +0800 Subject: [PATCH 21/27] fix missing label --- packages/hydrooj/src/handler/contest.ts | 14 ++++++-------- packages/hydrooj/src/handler/problem.ts | 2 +- packages/hydrooj/src/handler/record.ts | 4 ++-- packages/scoreboard-xcpcio/index.ts | 4 ++-- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 6ec7b46ea9..8cef8249cd 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -5,7 +5,7 @@ import { escapeRegExp, pick } from 'lodash'; import moment from 'moment-timezone'; import { ObjectId } from 'mongodb'; import { - Counter, diffArray, randomstring, sortFiles, Time, yaml, + Counter, diffArray, getAlphabeticId, randomstring, sortFiles, Time, yaml, } from '@hydrooj/utils/lib/utils'; import { Context, Service } from '../context'; import { @@ -821,14 +821,10 @@ export async function apply(ctx: Context) { }; const submissions = teams.flatMap((i, idx) => { if (!i.journal) return []; - const pid2idx = {}; - for (let j = 0; j < tdoc.problems.length; j++) { - pid2idx[tdoc.problems[j].pid] = j; - } - const journal = i.journal.filter((s) => pid2idx[s.pid] !== undefined); + const journal = i.journal.filter((s) => tdoc.pid2idx[s.pid] !== undefined); const c = Counter(); return journal.map((s) => { - const id = tdoc.problems[pid2idx[s.pid]].label; + const id = tdoc.problems[tdoc.pid2idx[s.pid]].label || getAlphabeticId(tdoc.pid2idx[s.pid]); c[id]++; return `@s ${idx + 1},${id},${c[id]},${time(s.rid)},${statusMap[s.status] || 'RJ'}`; }); @@ -840,7 +836,9 @@ export async function apply(ctx: Context) { `@teams ${tdoc.attend}`, `@submissions ${submissions.length}`, ].concat( - tdoc.problems.map((p) => `@p ${p.label},${escape(p.title || pdict[p.pid]?.title || 'Unknown Problem')},20,0`), + tdoc.problems.map((p, idx) => + `@p ${p.label || getAlphabeticId(idx)},${escape(p.title || pdict[p.pid]?.title || 'Unknown Problem')},20,0`, + ), teams.map((i, idx) => { const showName = this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) && udict[i.uid].displayName ? udict[i.uid].displayName : udict[i.uid].uname; diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 82349b4dd4..f31f2cef07 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -278,7 +278,7 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler { this.pdoc = await problem.get(domainId, pid); if (!this.pdoc) throw new ProblemNotFoundError(domainId, pid); if (tid) { - if (this.tdoc?.problems?.findIndex((p) => p.pid === this.pdoc.docId) === -1) throw new ContestNotFoundError(domainId, tid); + if (!Object.hasOwn(this.tdoc.pid2idx, this.pdoc.docId)) throw new ContestNotFoundError(domainId, tid); if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(tid); if (!contest.isDone(this.tdoc, this.tsdoc) && (!this.tsdoc?.attend || !this.tsdoc.startAt)) throw new ContestNotAttendedError(tid); // Delete problem-related info in contest mode diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index 905d7d6e82..e07c516a11 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -69,7 +69,7 @@ class RecordListHandler extends ContestDetailBaseHandler { } } // in order to make contest's submissionList can show label like A - const realPid = pid; + const pidOrLabel = pid; if (pid) { if (typeof pid === 'string' && tdoc) { const result = tdoc.problems.find((i, idx) => { @@ -120,7 +120,7 @@ class RecordListHandler extends ContestDetailBaseHandler { udict, all, allDomain, - filterPid: realPid, + filterPid: pidOrLabel, filterTid: tid, filterUidOrName: uidOrName, filterLang: lang, diff --git a/packages/scoreboard-xcpcio/index.ts b/packages/scoreboard-xcpcio/index.ts index cfaf0b9f7e..e68433449c 100644 --- a/packages/scoreboard-xcpcio/index.ts +++ b/packages/scoreboard-xcpcio/index.ts @@ -1,6 +1,6 @@ import path from 'path'; import { - ContestModel, Context, fs, ObjectId, PERM, Schema, STATUS, Types, UserModel, + ContestModel, Context, fs, getAlphabeticId, ObjectId, PERM, Schema, STATUS, Types, UserModel, } from 'hydrooj'; const file = fs.readFileSync(path.join(__dirname, 'public/assets/board.html'), 'utf8'); @@ -64,7 +64,7 @@ export async function apply(ctx: Context) { frozen_time: tdoc.lockAt ? Math.floor((tdoc.endAt.getTime() - tdoc.lockAt.getTime()) / 1000) : 0, penalty: 1200, problem_quantity: tdoc.problems.length, - problem_id: tdoc.problems.map(({ label }) => label), + problem_id: tdoc.problems.map((cp, idx) => cp.label || getAlphabeticId(idx)), group: { official: '正式队伍', unofficial: '打星队伍', From cc7b8b79b61d3da4bef1ec7399bf818574f139d6 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Fri, 18 Jul 2025 23:18:57 +0800 Subject: [PATCH 22/27] fix existed problems in review --- packages/hydrooj/src/handler/contest.ts | 9 +++------ packages/ui-default/templates/contest_balloon.html | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 8cef8249cd..58b8cf13fe 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -107,10 +107,7 @@ export class ContestDetailBaseHandler extends Handler { if (!tid || this.tdoc.rule === 'homework') return; if (this.request.json || !this.response.template) return; const pdoc = 'pdoc' in this ? (this as any).pdoc : {}; - const pid2idx = {}; - for (const [i, p] of this.tdoc.problems.entries()) { - pid2idx[p.pid] = i; - } + const cp = this.tdoc.problems[this.tdoc.pid2idx[pdoc.docId]]; this.response.body.overrideNav = [ { name: 'contest_main', @@ -136,7 +133,7 @@ export class ContestDetailBaseHandler extends Handler { }, { name: 'problem_detail', - displayName: `${this.tdoc.problems[pid2idx[pdoc.docId]]?.label}. ${this.tdoc.problems[pid2idx[pdoc.docId]]?.title ?? pdoc.title}`, + displayName: `${cp?.label || getAlphabeticId(this.tdoc.pid2idx[pdoc.docId])}. ${cp?.title || pdoc.title}`, args: { query: { tid }, pid: pdoc.docId, prefix: 'contest_detail_problem' }, checker: () => 'pdoc' in this, }, @@ -821,7 +818,7 @@ export async function apply(ctx: Context) { }; const submissions = teams.flatMap((i, idx) => { if (!i.journal) return []; - const journal = i.journal.filter((s) => tdoc.pid2idx[s.pid] !== undefined); + const journal = i.journal.filter((s) => Object.hasOwn(tdoc.pid2idx, s.pid)); const c = Counter(); return journal.map((s) => { const id = tdoc.problems[tdoc.pid2idx[s.pid]].label || getAlphabeticId(tdoc.pid2idx[s.pid]); diff --git a/packages/ui-default/templates/contest_balloon.html b/packages/ui-default/templates/contest_balloon.html index 04dadfea2a..26f2d2de6c 100644 --- a/packages/ui-default/templates/contest_balloon.html +++ b/packages/ui-default/templates/contest_balloon.html @@ -13,7 +13,7 @@

{{ _('Balloon Status') }}

{{ noscript_note.render() }}
- {% if tdoc.problems|length == 0 or not tdoc.problems[0].balloon %} + {% if tdoc.problems|length != 0 and not tdoc.problems[0].balloon %} {{ nothing.render('Please set the balloon color for each problem first.') }} {% else %}
From e55f1d33de54bc7d660cb425b88abe8a66f05394 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Mon, 21 Jul 2025 15:26:41 +0800 Subject: [PATCH 23/27] fix --- packages/hydrooj/src/handler/contest.ts | 4 +- packages/hydrooj/src/model/contest.ts | 2 +- .../ContestProblemEditor.tsx | 188 ++++++++++-------- packages/ui-default/package.json | 1 - .../ui-default/pages/contest_edit.page.tsx | 4 +- 5 files changed, 112 insertions(+), 87 deletions(-) diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 58b8cf13fe..0a43dc8533 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -133,7 +133,7 @@ export class ContestDetailBaseHandler extends Handler { }, { name: 'problem_detail', - displayName: `${cp?.label || getAlphabeticId(this.tdoc.pid2idx[pdoc.docId])}. ${cp?.title || pdoc.title}`, + displayName: `${cp?.label || getAlphabeticId(this.tdoc.pid2idx[pdoc.docId] || 0)}. ${cp?.title || pdoc.title}`, args: { query: { tid }, pid: pdoc.docId, prefix: 'contest_detail_problem' }, checker: () => 'pdoc' in this, }, @@ -834,7 +834,7 @@ export async function apply(ctx: Context) { `@submissions ${submissions.length}`, ].concat( tdoc.problems.map((p, idx) => - `@p ${p.label || getAlphabeticId(idx)},${escape(p.title || pdict[p.pid]?.title || 'Unknown Problem')},20,0`, + `@p ${escape(p.label || getAlphabeticId(idx))},${escape(p.title || pdict[p.pid]?.title || 'Unknown Problem')},20,0`, ), teams.map((i, idx) => { const showName = this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) && udict[i.uid].displayName diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index be86deed9f..3f861b51b5 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -511,7 +511,7 @@ const strictioi = buildContestRule({ for (const cp of tdoc.problems) { const pid = cp.pid; const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`; - const fullMark = tdoc.problems[tdoc.pid2idx[pid]]?.score || 100; + const fullMark = cp?.score || 100; const n: ScoreboardNode = (!config.isExport && !config.lockAt && isDone(tdoc) && meta?.psdict?.[index]?.rid && tsddict[pid]?.rid?.toHexString() !== meta?.psdict?.[index]?.rid?.toHexString() diff --git a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx index 0e4a3ea27f..4f3f0fab64 100644 --- a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx +++ b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx @@ -1,7 +1,8 @@ import { getAlphabeticId } from '@hydrooj/utils/lib/common'; -import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; import { debounce } from 'lodash'; import React from 'react'; +import { DndProvider, useDrag, useDrop } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; import { api, i18n } from 'vj/utils'; import ProblemSelectAutoComplete from '../autocomplete/components/ProblemSelectAutoComplete'; @@ -22,7 +23,86 @@ export interface ContestProblemEditorProps { onChange: (problems: Problem[]) => void; } const randomId = () => Math.random().toString(16).substring(2); -const ContestProblemEditor = ({ problems: initialProblems, onChange: _onChange } : ContestProblemEditorProps) => { +const ItemTypes = { + PROBLEM: 'problem', +}; + +interface DragItem { + index: number; + id: string; + type: string; +} + +const DraggableRow = ({ + problem, index, handleChange, handleRemove, problemRefs, problemRawTitles, moveRow, +}) => { + const [{ isDragging }, drag] = useDrag({ + type: ItemTypes.PROBLEM, + item: { type: ItemTypes.PROBLEM, id: problem._tmpId, index }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }); + + const [, drop] = useDrop({ + accept: ItemTypes.PROBLEM, + hover: (item: DragItem, monitor) => { + if (!monitor.isOver({ shallow: true })) return; + const dragIndex = item.index; + const hoverIndex = index; + if (dragIndex === hoverIndex) return; + moveRow(dragIndex, hoverIndex); + item.index = hoverIndex; + }, + }); + + return ( + drag(drop(node))} style={{ opacity: isDragging ? 0.5 : 1 }}> + + + + + + + + + ); +}; + +const ContestProblemEditor = ({ problems: initialProblems, onChange: _onChange }: ContestProblemEditorProps) => { // TODO: also support balloon and other fields in the future const [problems, setProblems] = React.useState(initialProblems.map((el, idx) => ({ ...el, @@ -100,11 +180,9 @@ const ContestProblemEditor = ({ problems: initialProblems, onChange: _onChange } onChange(newProblems); }; - const onDragEnd = (result) => { - if (!result.destination) return; - + const moveRow = (iX: number, iY: number) => { const newProblems = [...problems]; - const [iX, iY] = [result.source.index, result.destination.index]; + [newProblems[iX], newProblems[iY]] = [newProblems[iY], newProblems[iX]]; const [labelX, labelY] = [newProblems[iX].label, newProblems[iY].label]; newProblems[iX].label = labelY; @@ -115,8 +193,8 @@ const ContestProblemEditor = ({ problems: initialProblems, onChange: _onChange } }; return ( -
- + +
+ { problemRefs.current[index] = ref; }} + onChange={(v) => handleChange(index, 'pid', v)} + selectedKeys={[problem.pid.toString()]} + /> + {problemRawTitles[problem.pid]} + handleChange(index, 'title', e.target.value)} + placeholder={i18n('(leave blank if none)')} + /> + + handleChange(index, 'label', e.target.value)} + /> + + handleChange(index, 'score', parseInt(e.target.value, 10) || 0)} + min={0} + /> + + handleRemove(index)}> + {i18n('Remove')} + +
@@ -129,82 +207,28 @@ const ContestProblemEditor = ({ problems: initialProblems, onChange: _onChange } - - {(provided) => ( - - {problems.map((problem, index) => ( - - {(providedDrag, snapshot) => ( - - - - - - - - - - )} - - ))} - {provided.placeholder} - - )} - + + {problems.map((problem, index) => ( + + ))} +
{i18n('Action')}
- ⋮ - - { problemRefs.current[index] = ref; }} - onChange={(v) => handleChange(index, 'pid', v)} - selectedKeys={[problem.pid.toString()]} - /> - - {problemRawTitles[problem.pid]} - - handleChange(index, 'title', e.target.value)} - placeholder={i18n('(leave blank if none)')} - /> - - handleChange(index, 'label', e.target.value)} - /> - - handleChange(index, 'score', parseInt(e.target.value, 10) || 0)} - min={0} - /> - - handleRemove(index)}> - {i18n('Remove')} - -
- -
- - +
+ +
-
+ ); }; diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 6a21f31c1c..52868b0f16 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -20,7 +20,6 @@ "@fontsource/roboto-mono": "^5.2.6", "@fontsource/source-code-pro": "^5.2.6", "@fontsource/ubuntu-mono": "^5.2.6", - "@hello-pangea/dnd": "^18.0.1", "@hydrooj/utils": "workspace:^", "@sentry/browser": "^9.33.0", "@sentry/webpack-plugin": "^3.5.0", diff --git a/packages/ui-default/pages/contest_edit.page.tsx b/packages/ui-default/pages/contest_edit.page.tsx index a381296151..b3788576ec 100644 --- a/packages/ui-default/pages/contest_edit.page.tsx +++ b/packages/ui-default/pages/contest_edit.page.tsx @@ -9,7 +9,7 @@ import { confirm } from 'vj/components/dialog'; import { NamedPage } from 'vj/misc/Page'; import { i18n, request } from 'vj/utils'; -export default new NamedPage(['contest_edit', 'contest_create', 'homework_create', 'homework_edit'], (pagename) => { +const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', 'homework_edit'], (pagename) => { ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true, clearDefaultValue: false }); UserSelectAutoComplete.getOrConstruct($('[name="maintainer"]'), { multi: true, clearDefaultValue: false }); LanguageSelectAutoComplete.getOrConstruct($('[name=langs]'), { multi: true }); @@ -72,3 +72,5 @@ export default new NamedPage(['contest_edit', 'contest_create', 'homework_create }, 500); } }); + +export default page; From 9584fd863584e81e0a05565e7dbb1b8d99daef06 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Mon, 21 Jul 2025 18:41:44 +0800 Subject: [PATCH 24/27] strcuture optimize --- packages/hydrooj/src/handler/contest.ts | 81 ++++---- packages/hydrooj/src/handler/homework.ts | 30 +-- packages/hydrooj/src/handler/problem.ts | 2 +- packages/hydrooj/src/handler/record.ts | 8 +- packages/hydrooj/src/interface.ts | 12 +- packages/hydrooj/src/model/contest.ts | 180 ++++-------------- packages/hydrooj/src/upgrade.ts | 24 ++- packages/migrate/scripts/hustoj.ts | 2 +- packages/migrate/scripts/poj.ts | 2 +- packages/migrate/scripts/syzoj.ts | 2 +- packages/migrate/scripts/universaloj.ts | 2 +- packages/onsite-toolkit/index.ts | 22 +-- packages/scoreboard-xcpcio/index.ts | 12 +- .../ContestProblemEditor.tsx | 76 ++++---- .../ui-default/pages/contest_balloon.page.tsx | 12 +- .../ui-default/pages/contest_edit.page.tsx | 13 +- .../templates/components/contest.html | 2 +- .../ui-default/templates/contest_balloon.html | 2 +- .../ui-default/templates/contest_edit.html | 3 +- .../ui-default/templates/contest_manage.html | 13 +- .../templates/contest_problemlist.html | 9 +- .../ui-default/templates/homework_detail.html | 3 +- .../ui-default/templates/homework_edit.html | 5 +- .../templates/partials/contest_sidebar.html | 2 +- .../partials/contest_sidebar_management.html | 2 +- .../ui-default/templates/problem_detail.html | 8 +- packages/utils/lib/common.ts | 6 +- 27 files changed, 214 insertions(+), 321 deletions(-) diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 0a43dc8533..59a821ff69 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -13,7 +13,7 @@ import { ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, InvalidTokenError, NotAssignedError, NotFoundError, PermissionError, ValidationError, } from '../error'; -import { ScoreboardConfig, Tdoc } from '../interface'; +import { ContestProblemConfig, ScoreboardConfig, Tdoc } from '../interface'; import { PERM, PRIV, STATUS } from '../model/builtin'; import * as contest from '../model/contest'; import * as discussion from '../model/discussion'; @@ -107,7 +107,6 @@ export class ContestDetailBaseHandler extends Handler { if (!tid || this.tdoc.rule === 'homework') return; if (this.request.json || !this.response.template) return; const pdoc = 'pdoc' in this ? (this as any).pdoc : {}; - const cp = this.tdoc.problems[this.tdoc.pid2idx[pdoc.docId]]; this.response.body.overrideNav = [ { name: 'contest_main', @@ -133,7 +132,7 @@ export class ContestDetailBaseHandler extends Handler { }, { name: 'problem_detail', - displayName: `${cp?.label || getAlphabeticId(this.tdoc.pid2idx[pdoc.docId] || 0)}. ${cp?.title || pdoc.title}`, + displayName: `${this.tdoc.problemConfig[pdoc.docId]?.label || getAlphabeticId(this.tdoc.pids.indexOf(pdoc.docId))}. ${pdoc.title}`, args: { query: { tid }, pid: pdoc.docId, prefix: 'contest_detail_problem' }, checker: () => 'pdoc' in this, }, @@ -189,7 +188,7 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler { if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(domainId, tid); if (!this.tsdoc?.attend && !contest.isDone(this.tdoc)) throw new ContestNotAttendedError(domainId, tid); const [pdict, udict, tcdocs] = await Promise.all([ - problem.getList(domainId, this.tdoc.problems.map((p) => p.pid), true, true, problem.PROJECTION_CONTEST_LIST), + problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST), user.getList(domainId, [this.tdoc.owner, this.user._id]), contest.getMultiClarification(domainId, tid, this.user._id), ]); @@ -197,7 +196,7 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler { pdict, psdict: {}, udict, rdict: {}, tdoc: this.tdoc, tcdocs, }; this.response.template = 'contest_problemlist.html'; - this.response.body.showScore = this.tdoc.problems.some((p) => p.score && p.score !== 100); + this.response.body.showScore = Object.values(this.tdoc.problemConfig || {}).some((cp) => cp.score && cp.score !== 100); if (!this.tsdoc) return; if (this.tsdoc.attend && !this.tsdoc.startAt && contest.isOngoing(this.tdoc)) { await contest.setStatus(domainId, tid, this.user._id, { startAt: new Date() }); @@ -210,7 +209,7 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler { this.response.body.canViewRecord = canViewRecord; const rids = psdocs.map((i) => i.rid); if (contest.isDone(this.tdoc) && canViewRecord) { - const correction = await problem.getListStatus(domainId, this.user._id, this.tdoc.problems.map((p) => p.pid)); + const correction = await problem.getListStatus(domainId, this.user._id, this.tdoc.pids); for (const pid in correction) { if (this.tsdoc.detail?.[pid]?.rid === correction[pid].rid) delete correction[pid]; } @@ -246,7 +245,7 @@ export class ContestProblemListHandler extends ContestDetailBaseHandler { if (!this.user.own(this.tdoc)) { await message.send(1, this.tdoc.maintainer.concat(this.tdoc.owner), JSON.stringify({ message: 'Contest {0} has a new clarification about {1}, please go to contest management to reply.', - params: [this.tdoc.title, subject > 0 ? `#${this.tdoc.pid2idx[subject] + 1}` : 'the contest'], + params: [this.tdoc.title, subject > 0 ? `#${this.tdoc.pids.indexOf(subject) + 1}` : 'the contest'], url: this.url('contest_manage', { tid }), }), message.FLAG_I18N | message.FLAG_UNREAD); } @@ -284,7 +283,8 @@ export class ContestEditHandler extends Handler { rules, tdoc: this.tdoc, duration: tid ? -beginAt.diff(this.tdoc.endAt, 'hour', true) : 2, - problems: tid ? this.tdoc.problems : [], + pids: tid ? this.tdoc.pids.join(',') : '', + problemConfig: tid ? this.tdoc.problemConfig : {}, beginAt, page_name: tid ? 'contest_edit' : 'contest_create', }; @@ -297,7 +297,8 @@ export class ContestEditHandler extends Handler { @param('title', Types.Title) @param('content', Types.Content) @param('rule', Types.Range(Object.keys(contest.RULES).filter((i) => !contest.RULES[i].hidden))) - @param('problems', Types.Content) + @param('pids', Types.Content) + @param('problemConfig', Types.Content) @param('rated', Types.Boolean) @param('code', Types.String, true) @param('autoHide', Types.Boolean) @@ -309,13 +310,18 @@ export class ContestEditHandler extends Handler { @param('langs', Types.CommaSeperatedArray, true) async postUpdate( domainId: string, tid: ObjectId, beginAtDate: string, beginAtTime: string, duration: number, - title: string, content: string, rule: string, _problems: string, rated = false, + title: string, content: string, rule: string, _pids: string, _problemConfig: string, rated = false, _code = '', autoHide = false, assign: string[] = [], lock: number = null, contestDuration: number = null, maintainer: number[] = [], allowViewCode = false, langs: string[] = [], ) { if (autoHide) this.checkPerm(PERM.PERM_EDIT_PROBLEM); - const { problems, score } = contest.resolveContestProblemJson(_problems); - const pids = problems.map((p) => p.pid); + let problemConfig = {} as Record; + try { + problemConfig = JSON.parse(_problemConfig); + } catch (e) { + throw new ValidationError('problemConfig'); + } + const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i); const beginAtMoment = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone); if (!beginAtMoment.isValid()) throw new ValidationError('beginAtDate', 'beginAtTime'); const endAt = beginAtMoment.clone().add(duration, 'hours').toDate(); @@ -326,17 +332,17 @@ export class ContestEditHandler extends Handler { await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id, true); if (tid) { await contest.edit(domainId, tid, { - title, content, rule, beginAt, endAt, pids, problems, score, rated, duration: contestDuration, + title, content, rule, beginAt, endAt, pids, problemConfig, rated, duration: contestDuration, }); if (this.tdoc.beginAt !== beginAt || this.tdoc.endAt !== endAt - || diffArray(this.tdoc.problems.map((i) => i.pid), pids) || this.tdoc.rule !== rule + || diffArray(this.tdoc.pids, pids) || this.tdoc.rule !== rule || lockAt !== this.tdoc.lockAt) { await contest.recalcStatus(domainId, this.tdoc.docId); } } else { tid = await contest.add( - domainId, title, content, this.user._id, rule, beginAt, endAt, pids, rated, - { duration: contestDuration, score, problems }, + domainId, title, content, this.user._id, rule, beginAt, endAt, pids, problemConfig, rated, + { duration: contestDuration }, ); } const task = { @@ -443,7 +449,7 @@ export class ContestManagementHandler extends ContestManagementBaseHandler { tdoc: this.tdoc, tsdoc: this.tsdoc, owner_udoc: await user.getById(domainId, this.tdoc.owner), - pdict: await problem.getList(domainId, this.tdoc.problems.map((i) => i.pid), true, true, [...problem.PROJECTION_CONTEST_LIST, 'tag']), + pdict: await problem.getList(domainId, this.tdoc.pids, true, true, [...problem.PROJECTION_CONTEST_LIST, 'tag']), files: sortFiles(this.tdoc.files || []), udict: await user.getListForRender( domainId, tcdocs.map((i) => i.owner), @@ -521,17 +527,10 @@ export class ContestManagementHandler extends ContestManagementBaseHandler { @param('pid', Types.PositiveInt) @param('score', Types.PositiveInt) async postSetScore(domainId: string, pid: number, score: number) { - const idx = this.tdoc.pid2idx[pid]; - if (idx === undefined) throw new ValidationError('pid'); - this.tdoc.problems[idx].score = score; - // TODO: remove `score` field later - this.tdoc.score = this.tdoc.problems.reduce( - (acc, cur) => { - if (cur.score) acc[cur.pid] = cur.score; - return acc; - }, {}, - ); - await contest.edit(domainId, this.tdoc.docId, { score: this.tdoc.score, problems: this.tdoc.problems }); + if (!this.tdoc.pids.includes(pid)) throw new ValidationError('pid'); + this.tdoc.problemConfig[pid] ||= {}; + this.tdoc.problemConfig[pid].score = score; + await contest.edit(domainId, this.tdoc.docId, { problemConfig: this.tdoc.problemConfig }); await contest.recalcStatus(domainId, this.tdoc.docId); this.back(); } @@ -605,7 +604,7 @@ export class ContestBalloonHandler extends ContestManagementBaseHandler { tdoc: this.tdoc, tsdoc: this.tsdoc, owner_udoc: await user.getById(domainId, this.tdoc.owner), - pdict: await problem.getList(domainId, this.tdoc.problems.map((i) => i.pid), true, true, problem.PROJECTION_CONTEST_LIST), + pdict: await problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST), bdocs, udict: await user.getListForRender(domainId, uids, this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) ? ['displayName'] : []), }; @@ -618,14 +617,12 @@ export class ContestBalloonHandler extends ContestManagementBaseHandler { async postSetColor(domainId: string, tid: ObjectId, color: string) { const config = yaml.load(color); if (typeof config !== 'object') throw new ValidationError('color'); - const balloon = {}; - for (let i = 0; i < this.tdoc.problems.length; i++) { - const pid = this.tdoc.problems[i].pid; + for (const pid of this.tdoc.pids) { if (!config[pid]) throw new ValidationError('color'); - balloon[pid] = config[pid.toString()]; - this.tdoc.problems[i].balloon = config[pid.toString()]; + this.tdoc.problemConfig[pid] ||= {}; + this.tdoc.problemConfig[pid].balloon = config[pid.toString()]; } - await contest.edit(domainId, tid, { balloon, problems: this.tdoc.problems }); + await contest.edit(domainId, tid, { problemConfig: this.tdoc.problemConfig }); this.back(); } @@ -759,7 +756,7 @@ export async function apply(ctx: Context) { const tasks = []; for (const op of doc.operation) { if (op === 'unhide') { - for (const pid of tdoc.problems.map((i) => i.pid)) { + for (const pid of tdoc.pids) { tasks.push(problem.edit(doc.domainId, pid, { hidden: false })); } } @@ -800,7 +797,7 @@ export async function apply(ctx: Context) { this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); } const [pdict, teams] = await Promise.all([ - problem.getList(tdoc.domainId, tdoc.problems.map((i) => i.pid), true, false, problem.PROJECTION_LIST, true), + problem.getList(tdoc.domainId, tdoc.pids, true, false, problem.PROJECTION_LIST, true), contest.getMultiStatus(tdoc.domainId, { docId: tdoc._id }).toArray(), ]); const udict = await user.getList(tdoc.domainId, teams.map((i) => i.uid)); @@ -818,10 +815,10 @@ export async function apply(ctx: Context) { }; const submissions = teams.flatMap((i, idx) => { if (!i.journal) return []; - const journal = i.journal.filter((s) => Object.hasOwn(tdoc.pid2idx, s.pid)); + const journal = i.journal.filter((s) => tdoc.pids.includes(s.pid)); const c = Counter(); return journal.map((s) => { - const id = tdoc.problems[tdoc.pid2idx[s.pid]].label || getAlphabeticId(tdoc.pid2idx[s.pid]); + const id = tdoc.problemConfig[s.pid]?.label || getAlphabeticId(tdoc.pids.indexOf(s.pid)); c[id]++; return `@s ${idx + 1},${id},${c[id]},${time(s.rid)},${statusMap[s.status] || 'RJ'}`; }); @@ -829,12 +826,12 @@ export async function apply(ctx: Context) { const res = [ `@contest "${escape(tdoc.title)}"`, `@contlen ${Math.floor((tdoc.endAt.getTime() - tdoc.beginAt.getTime()) / Time.minute)}`, - `@problems ${tdoc.problems.length}`, + `@problems ${tdoc.pids.length}`, `@teams ${tdoc.attend}`, `@submissions ${submissions.length}`, ].concat( - tdoc.problems.map((p, idx) => - `@p ${escape(p.label || getAlphabeticId(idx))},${escape(p.title || pdict[p.pid]?.title || 'Unknown Problem')},20,0`, + tdoc.pids.map((pid, idx) => + `@p ${escape(tdoc.problemConfig[pid]?.label || getAlphabeticId(idx))},${escape(pdict[pid]?.title || 'Unknown Problem')},20,0`, ), teams.map((i, idx) => { const showName = this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) && udict[i.uid].displayName diff --git a/packages/hydrooj/src/handler/homework.ts b/packages/hydrooj/src/handler/homework.ts index af657002b7..a01f699b5b 100644 --- a/packages/hydrooj/src/handler/homework.ts +++ b/packages/hydrooj/src/handler/homework.ts @@ -6,7 +6,7 @@ import { diffArray, sortFiles, Time } from '@hydrooj/utils/lib/utils'; import { ContestNotFoundError, FileLimitExceededError, FileUploadError, HomeworkNotLiveError, NotAssignedError, ValidationError, } from '../error'; -import { PenaltyRules, Tdoc } from '../interface'; +import { ContestProblemConfig, PenaltyRules, Tdoc } from '../interface'; import { PERM } from '../model/builtin'; import * as contest from '../model/contest'; import * as discussion from '../model/discussion'; @@ -116,7 +116,7 @@ class HomeworkDetailHandler extends Handler { && !this.user.own(this.tdoc) && !this.user.hasPerm(PERM.PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD) ) return; - const pdict = await problem.getList(domainId, this.tdoc.problems.map((p) => p.pid), true, true, problem.PROJECTION_CONTEST_LIST); + const pdict = await problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST); const psdict = {}; let rdict = {}; if (tsdoc) { @@ -173,7 +173,8 @@ class HomeworkEditHandler extends Handler { timePenaltyText: penaltySince.format('H:mm'), extensionDays, penaltyRules: tid ? yaml.dump(tdoc.penaltyRules) : null, - problems: tid ? tdoc.problems : [], + pids: tid ? tdoc.pids.join(',') : '', + problemConfig: tid ? tdoc.problemConfig : [], page_name: tid ? 'homework_edit' : 'homework_create', }; } @@ -187,7 +188,8 @@ class HomeworkEditHandler extends Handler { @param('penaltyRules', Types.Content, validatePenaltyRules, convertPenaltyRules) @param('title', Types.Title) @param('content', Types.Content) - @param('problems', Types.Content) + @param('pids', Types.Content) + @param('problemConfig', Types.Content) @param('rated', Types.Boolean) @param('maintainer', Types.NumericArray, true) @param('assign', Types.CommaSeperatedArray, true) @@ -195,11 +197,16 @@ class HomeworkEditHandler extends Handler { async postUpdate( domainId: string, tid: ObjectId, beginAtDate: string, beginAtTime: string, penaltySinceDate: string, penaltySinceTime: string, extensionDays: number, - penaltyRules: PenaltyRules, title: string, content: string, _problems: string, rated = false, + penaltyRules: PenaltyRules, title: string, content: string, _pids: string, _problemConfig: string, rated = false, maintainer: number[] = [], assign: string[] = [], langs: string[] = [], ) { - const { problems, score } = contest.resolveContestProblemJson(_problems); - const pids = problems.map((p) => p.pid); + let problemConfig = {} as Record; + try { + problemConfig = JSON.parse(_problemConfig); + } catch (e) { + throw new ValidationError('problemConfig'); + } + const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i); const tdoc = tid ? await contest.get(domainId, tid) : null; if (!tid) this.checkPerm(PERM.PERM_CREATE_HOMEWORK); else if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_HOMEWORK); @@ -214,9 +221,9 @@ class HomeworkEditHandler extends Handler { await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id, true); if (!tid) { tid = await contest.add(domainId, title, content, this.user._id, - 'homework', beginAt.toDate(), endAt.toDate(), pids, rated, + 'homework', beginAt.toDate(), endAt.toDate(), pids, problemConfig, rated, { - penaltySince: penaltySince.toDate(), penaltyRules, assign, problems, score, + penaltySince: penaltySince.toDate(), penaltyRules, assign, }); } else { await contest.edit(domainId, tid, { @@ -225,8 +232,7 @@ class HomeworkEditHandler extends Handler { beginAt: beginAt.toDate(), endAt: endAt.toDate(), pids, - problems, - score, + problemConfig, penaltySince: penaltySince.toDate(), penaltyRules, rated, @@ -237,7 +243,7 @@ class HomeworkEditHandler extends Handler { if (tdoc.beginAt !== beginAt.toDate() || tdoc.endAt !== endAt.toDate() || tdoc.penaltySince !== penaltySince.toDate() - || diffArray(tdoc.problems.map((i) => i.pid), pids)) { + || diffArray(tdoc.pids, pids)) { await contest.recalcStatus(domainId, tdoc.docId); } } diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index c56b7916c6..20bc3b5324 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -279,7 +279,7 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler { this.pdoc = await problem.get(domainId, pid); if (!this.pdoc) throw new ProblemNotFoundError(domainId, pid); if (tid) { - if (!Object.hasOwn(this.tdoc.pid2idx, this.pdoc.docId)) throw new ContestNotFoundError(domainId, tid); + if (!this.tdoc?.pids?.includes(this.pdoc.docId)) throw new ContestNotFoundError(domainId, tid); if (contest.isNotStarted(this.tdoc)) throw new ContestNotLiveError(tid); if (!contest.isDone(this.tdoc, this.tsdoc) && (!this.tsdoc?.attend || !this.tsdoc.startAt)) throw new ContestNotAttendedError(tid); // Delete problem-related info in contest mode diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index 0020aad3da..9a609d3ff1 100644 --- a/packages/hydrooj/src/handler/record.ts +++ b/packages/hydrooj/src/handler/record.ts @@ -40,7 +40,7 @@ class RecordListHandler extends ContestDetailBaseHandler { all = false, allDomain = false, ) { const notification = []; - let tdoc = null; + let tdoc : null | Tdoc = null; let invalid = false; this.response.template = 'record_main.html'; const q: Filter = { contest: tid }; @@ -72,11 +72,11 @@ class RecordListHandler extends ContestDetailBaseHandler { const pidOrLabel = pid; if (pid) { if (typeof pid === 'string' && tdoc) { - const result = tdoc.problems.find((i, idx) => { - if (i.label) return i.label === pid; + const result = tdoc.pids.find((_pid, idx) => { + if (tdoc.problemConfig[_pid]?.label) return tdoc.problemConfig[_pid]?.label === pid; return pid === getAlphabeticId(idx); }); - if (result) pid = result.pid; + if (result) pid = result; } const pdoc = await problem.get(domainId, pid); if (pdoc) q.pid = pdoc.docId; diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 14191380fb..5ec742738b 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -247,13 +247,11 @@ export interface TrainingNode { pids: number[], } -export interface ContestProblem { - pid: number; +export interface ContestProblemConfig { label?: string; - title?: string; score?: number; balloon?: { color: string, name: string }; - [key: string]: any, + // [key: string]: any, } export interface Tdoc extends Document { @@ -265,9 +263,8 @@ export interface Tdoc extends Document { title: string; content: string; rule: string; - /** @deprecated */ pids: number[]; - problems: ContestProblem[]; + problemConfig: Record; rated?: boolean; _code?: string; assign?: string[]; @@ -297,9 +294,6 @@ export interface Tdoc extends Document { // For training description?: string; dag?: TrainingNode[]; - - // not stored in database - pid2idx?: Record; } export interface TrainingDoc extends Omit { diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 3f861b51b5..363817a518 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -1,6 +1,5 @@ import { sumBy } from 'lodash'; import { Filter, ObjectId } from 'mongodb'; -import Schema from 'schemastery'; import { Counter, formatSeconds, getAlphabeticId, Time, } from '@hydrooj/utils/lib/utils'; @@ -9,7 +8,7 @@ import { ContestScoreboardHiddenError, ValidationError, } from '../error'; import { - BaseUserDict, ContestProblem, ContestRule, ContestRules, ProblemDict, RecordDoc, + BaseUserDict, ContestProblemConfig, ContestRule, ContestRules, ProblemDict, RecordDoc, ScoreboardConfig, ScoreboardNode, ScoreboardRow, SubtaskResult, Tdoc, } from '../interface'; import bus from '../service/bus'; @@ -104,7 +103,7 @@ const acm = buildContestRule({ let time = 0; const lockAt = isLocked(tdoc) ? tdoc.lockAt : null; for (const j of journal) { - if (!Object.hasOwn(tdoc.pid2idx, j.pid)) continue; + if (!tdoc.pids.includes(j.pid)) continue; if (!this.submitAfterAccept && display[j.pid]?.status === STATUS.STATUS_ACCEPTED) continue; if (![STATUS.STATUS_ACCEPTED, STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_FORMAT_ERROR, STATUS.STATUS_CANCELED].includes(j.status)) { naccept[j.pid]++; @@ -144,9 +143,8 @@ const acm = buildContestRule({ columns.push({ type: 'string', value: _('Student ID') }); } columns.push({ type: 'solved', value: `${_('Solved')}\n${_('Total Time')}` }); - for (let i = 1; i <= tdoc.problems.length; i++) { - const cp = tdoc.problems[i - 1]; - const pid = cp.pid; + for (let i = 1; i <= tdoc.pids.length; i++) { + const pid = tdoc.pids[i - 1]; pdict[pid].nAccept = pdict[pid].nSubmit = 0; if (config.isExport) { columns.push( @@ -162,7 +160,7 @@ const acm = buildContestRule({ } else { columns.push({ type: 'problem', - value: cp.label || getAlphabeticId(i - 1), + value: tdoc.problemConfig[pid]?.label || getAlphabeticId(i - 1), raw: pid, }); } @@ -196,8 +194,7 @@ const acm = buildContestRule({ } } const tsddict = (config.lockAt ? tsdoc.display : tsdoc.detail) || {}; - for (const p of tdoc.problems) { - const pid = p.pid; + 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() : ''; @@ -286,7 +283,7 @@ const oi = buildContestRule({ let score = 0; const lockAt = isLocked(tdoc) ? tdoc.lockAt : null; - for (const j of journal.filter((i) => Object.hasOwn(tdoc.pid2idx, i.pid))) { + for (const j of journal.filter((i) => tdoc.pids.includes(i.pid))) { if (lockAt && j.rid.getTimestamp() > lockAt) { npending[j.pid]++; display[j.pid] ||= {}; @@ -299,7 +296,7 @@ const oi = buildContestRule({ } } for (const i in display) { - score += ((tdoc.problems[tdoc.pid2idx[Number(i)]]?.score || 100) * (display[i].score || 0)) / 100; + score += ((tdoc.problemConfig[Number(i)]?.score || 100) * (display[i].score || 0)) / 100; } return { score, detail, display }; }, @@ -318,20 +315,20 @@ const oi = buildContestRule({ columns.push({ type: 'string', value: _('Student ID') }); } columns.push({ type: 'total_score', value: _('Total Score') }); - for (let i = 1; i <= tdoc.problems.length; i++) { - const cp = tdoc.problems[i - 1]; - const pid = cp.pid; + for (let i = 1; i <= tdoc.pids.length; i++) { + const pid = tdoc.pids[i - 1]; + const cp = tdoc.problemConfig[pid]; pdict[pid].nAccept = pdict[pid].nSubmit = 0; if (config.isExport) { columns.push({ type: 'string', - value: '#{0} {1}'.format(i, cp.title || pdict[pid].title), + value: '#{0} {1}'.format(i, pdict[pid].title), }); } else { columns.push({ type: 'problem', - value: cp.label || getAlphabeticId(i - 1), - raw: cp.pid, + value: cp?.label || getAlphabeticId(i - 1), + raw: pid, }); } } @@ -344,7 +341,7 @@ const oi = buildContestRule({ ]; const displayScore = (pid: number, score?: number) => { if (typeof score !== 'number') return '-'; - return score * ((tdoc.problems[tdoc.pid2idx[pid]]?.score || 100) / 100); + return score * ((tdoc.problemConfig[pid]?.score || 100) / 100); }; if (config.isExport && config.showDisplayName) { row.push({ type: 'email', value: udoc.mail }); @@ -364,8 +361,7 @@ const oi = buildContestRule({ } } const tsddict = ((config.lockAt && isLocked(tdoc, new Date())) ? tsdoc.display : tsdoc.detail) || {}; - for (const cp of tdoc.problems) { - const pid = cp.pid; + for (const pid of tdoc.pids) { const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`; const node: ScoreboardNode = (!config.isExport && !config.lockAt && isDone(tdoc) @@ -416,7 +412,7 @@ const oi = buildContestRule({ if (isDone(tdoc)) { const psdocs = await Promise.all( - tdoc.problems.map(({ pid }) => problem.getMultiStatus(tdoc.domainId, { docId: pid, uid: { $in: uids } }).toArray()), + tdoc.pids.map((pid) => problem.getMultiStatus(tdoc.domainId, { docId: pid, uid: { $in: uids } }).toArray()), ); for (const tpsdoc of psdocs) { for (const psdoc of tpsdoc) { @@ -474,7 +470,7 @@ const strictioi = buildContestRule({ const detail = {}; let score = 0; const subtasks: Record> = {}; - for (const j of journal.filter((i) => Object.hasOwn(tdoc.pid2idx, i.pid))) { + for (const j of journal.filter((i) => tdoc.pids.includes(i.pid))) { subtasks[j.pid] ||= {}; for (const i in j.subtasks) { if (!subtasks[j.pid][i] || subtasks[j.pid][i].score < j.subtasks[i].score) subtasks[j.pid][i] = j.subtasks[i]; @@ -483,7 +479,7 @@ const strictioi = buildContestRule({ j.status = Math.max(...Object.values(subtasks[j.pid]).map((i) => i.status)); if (!detail[j.pid] || detail[j.pid].score < j.score) detail[j.pid] = { ...j, subtasks: subtasks[j.pid] }; } - for (const i in detail) score += ((tdoc.problems[tdoc.pid2idx[Number(i)]]?.score || 100) * (detail[i].score || 0)) / 100; + for (const i in detail) score += ((tdoc.problemConfig[Number(i)]?.score || 100) * (detail[i].score || 0)) / 100; return { score, detail }; }, async scoreboardRow(config, _, tdoc, pdict, udoc, rank, tsdoc, meta) { @@ -508,10 +504,9 @@ const strictioi = buildContestRule({ accepted[s.pid] = true; } } - for (const cp of tdoc.problems) { - const pid = cp.pid; + for (const pid of tdoc.pids) { const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`; - const fullMark = cp?.score || 100; + const fullMark = tdoc.problemConfig[pid]?.score || 100; const n: ScoreboardNode = (!config.isExport && !config.lockAt && isDone(tdoc) && meta?.psdict?.[index]?.rid && tsddict[pid]?.rid?.toHexString() !== meta?.psdict?.[index]?.rid?.toHexString() @@ -557,7 +552,7 @@ const ledo = buildContestRule({ stat(tdoc, journal) { const ntry = Counter(); const detail = {}; - for (const j of journal.filter((i) => Object.hasOwn(tdoc.pid2idx, i.pid))) { + for (const j of journal.filter((i) => tdoc.pids.includes(i.pid))) { const vaild = ![STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_FORMAT_ERROR].includes(j.status); if (vaild) ntry[j.pid]++; const penaltyScore = vaild ? Math.round(Math.max(0.7, 0.95 ** (ntry[j.pid] - 1)) * j.score) : 0; @@ -571,10 +566,9 @@ const ledo = buildContestRule({ } let score = 0; let originalScore = 0; - for (const cp of tdoc.problems) { - const pid = cp.pid; + for (const pid of tdoc.pids) { if (!detail[pid]) continue; - const rate = (tdoc.problems[tdoc.pid2idx[pid]]?.score || 100) / 100; + const rate = (tdoc.problemConfig[pid]?.score || 100) / 100; score += detail[pid].penaltyScore * rate; originalScore += detail[pid].score * rate; } @@ -608,11 +602,10 @@ const ledo = buildContestRule({ accepted[s.pid] = true; } } - for (const cp of tdoc.problems) { - const pid = cp.pid; + for (const pid of tdoc.pids) { row.push({ type: 'record', - value: ((tsddict[pid]?.penaltyScore || 0) * ((tdoc.problems[tdoc.pid2idx[pid]]?.score || 100) / 100)).toString(), + value: ((tsddict[pid]?.penaltyScore || 0) * ((tdoc.problemConfig[pid]?.score || 100) / 100)).toString(), hover: tsddict[pid]?.ntry ? `-${tsddict[pid].ntry} (${Math.round(Math.max(0.7, 0.95 ** tsddict[pid].ntry) * 100)}%)` : '', raw: tsddict[pid]?.rid, score: tsddict[pid]?.score, @@ -638,7 +631,7 @@ const homework = buildContestRule({ stat: (tdoc, journal) => { const effective = {}; for (const j of journal) { - if (Object.hasOwn(tdoc.pid2idx, j.pid)) effective[j.pid] = j; + if (tdoc.pids.find((pid) => pid === j.pid)) effective[j.pid] = j; } function time(jdoc) { const real = (jdoc.rid.getTimestamp().getTime() - tdoc.beginAt.getTime()) / 1000; @@ -646,7 +639,7 @@ const homework = buildContestRule({ } function penaltyScore(jdoc) { - const rate = (tdoc.problems[tdoc.pid2idx[Number(jdoc.pid)]]?.score || 100) / 100; + const rate = (tdoc.problemConfig[Number(jdoc.pid)]?.score || 100) / 100; const exceedSeconds = Math.floor( (jdoc.rid.getTimestamp().getTime() - tdoc.penaltySince.getTime()) / 1000, ); @@ -691,15 +684,14 @@ const homework = buildContestRule({ columns.push({ type: 'string', value: _('Original Score') }); } columns.push({ type: 'time', value: _('Total Time') }); - for (let i = 1; i <= tdoc.problems.length; i++) { - const cp = tdoc.problems[i - 1]; - const pid = cp.pid; + for (let i = 1; i <= tdoc.pids.length; i++) { + const pid = tdoc.pids[i - 1]; pdict[pid].nAccept = pdict[pid].nSubmit = 0; if (config.isExport) { columns.push( { type: 'string', - value: '#{0} {1}'.format(i, cp.title || pdict[pid].title), + value: '#{0} {1}'.format(i, pdict[pid].title), }, { type: 'string', @@ -713,7 +705,7 @@ const homework = buildContestRule({ } else { columns.push({ type: 'problem', - value: cp.label || getAlphabeticId(i - 1), + value: tdoc.problemConfig[pid]?.label || getAlphabeticId(i - 1), raw: pid, }); } @@ -750,8 +742,7 @@ const homework = buildContestRule({ accepted[s.pid] = true; } } - for (const cp of tdoc.problems) { - const pid = cp.pid; + for (const pid of tdoc.pids) { const rid = tsddict[pid]?.rid; const colScore = tsddict[pid]?.penaltyScore ?? ''; const colOriginalScore = tsddict[pid]?.score ?? ''; @@ -807,25 +798,17 @@ function _getStatusJournal(tsdoc) { export async function add( domainId: string, title: string, content: string, owner: number, rule: string, beginAt = new Date(), endAt = new Date(), - pids: number[] = [], rated = false, data: Partial = {}, + pids: number[] = [], problemConfig: Record = {}, rated = false, data: Partial = {}, ) { if (!RULES[rule]) throw new ValidationError('rule'); if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt'); - // TODO: this is the best way to support old plugins, but need remove one day - let problems = data.problems || []; - if (problems.length === 0 && pids.length > 0) { - problems = pids.map((pid) => ({ - pid, - ...(data.score?.[pid] ? { score: data.score[pid] } : {}), - })); - } Object.assign(data, { - content, owner, title, rule, beginAt, endAt, pids, problems, attend: 0, + content, owner, title, rule, beginAt, endAt, pids, problemConfig, attend: 0, }); RULES[rule].check(data); await bus.parallel('contest/before-add', data); const docId = await document.add(domainId, content, owner, document.TYPE_CONTEST, null, null, null, { - assign: [], ...data, title, rule, beginAt, endAt, pids, problems, attend: 0, rated, + assign: [], ...data, title, rule, beginAt, endAt, pids, problemConfig, attend: 0, rated, }); await bus.parallel('contest/add', data, docId); return docId; @@ -836,46 +819,6 @@ export async function edit(domainId: string, tid: ObjectId, $set: Partial) const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); if (!tdoc) throw new ContestNotFoundError(domainId, tid); RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set)); - // TODO: this is the best way to support old plugins, but need remove one day - if ($set.pids && !$set.problems) { - const mergedScore = { - ...(tdoc.score ? tdoc.score : {}), - ...(tdoc.problems.reduce((acc, cur) => { - if (cur.score) acc[cur.pid] = cur.score; - return acc; - }, {})), - ...($set.score ? $set.score : {}), - }; - $set.problems = $set.pids.map((pid) => ({ - pid, - ...(mergedScore[pid] ? { score: mergedScore[pid] } : {}), - ...($set.balloon - ? ( - typeof $set.balloon[pid] === 'string' ? { - balloon: { - color: $set.balloon[pid], - name: '', - }, - } : { balloon: $set.balloon[pid] } - ) : {}), - })); - } - if (!$set.problems && !$set.pids && ($set.balloon || $set.score)) { - $set.problems = tdoc.problems.map((p) => ({ - ...p, - ...(p.score || ($set.score && $set.score[p.pid]) ? { score: $set.score[p.pid] || p.score } : {}), - ...(p.balloon || ($set.balloon && $set.balloon[p.pid]) ? { - balloon: $set.balloon[p.pid] ? ( - typeof $set.balloon[p.pid] === 'string' ? { - balloon: { - color: $set.balloon[p.pid], - name: '', - }, - } : { balloon: $set.balloon[p.pid] } - ) : p.balloon, - } : {}), - })); - } const res = await document.set(domainId, document.TYPE_CONTEST, tid, $set); await bus.parallel('contest/edit', res); return res; @@ -892,13 +835,7 @@ export async function del(domainId: string, tid: ObjectId) { export async function get(domainId: string, tid: ObjectId): Promise { const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); if (!tdoc) throw new ContestNotFoundError(tid); - return { - ...tdoc, - pid2idx: tdoc.problems.reduce((acc, cur, idx) => { - acc[cur.pid] = idx; - return acc; - }, {}), - }; + return tdoc; } export async function getRelated(domainId: string, pid: number, rule?: string) { @@ -946,7 +883,7 @@ export async function updateStatus( }: { status?: STATUS, score?: number, subtasks?: Record, lang?: string } = {}, ) { const tdoc = await get(domainId, tid); - if (tdoc.problems[tdoc.pid2idx[pid]]?.balloon && status === STATUS.STATUS_ACCEPTED) await addBalloon(domainId, tid, uid, rid, pid); + if (tdoc.problemConfig[pid]?.balloon && status === STATUS.STATUS_ACCEPTED) await addBalloon(domainId, tid, uid, rid, pid); const tsdoc = await document.revPushStatus(tdoc.domainId, document.TYPE_CONTEST, tdoc.docId, uid, 'journal', { rid, pid, status, score, subtasks, lang, }, 'rid'); @@ -1060,7 +997,7 @@ export async function getScoreboard( 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.problems.map((p) => p.pid), true, true, problem.PROJECTION_CONTEST_DETAIL); + const pdict = await problem.getList(domainId, tdoc.pids, true, true, problem.PROJECTION_CONTEST_DETAIL); const [rows, udict] = await RULES[tdoc.rule].scoreboard( config, this.translate.bind(this), tdoc, pdict, tsdocsCursor, @@ -1114,43 +1051,6 @@ export const statusText = (tdoc: Tdoc, tsdoc?: any) => ( ? 'Live...' : 'Done'); -export const ContestProblemJsonSchema = Schema.array(Schema.object({ - pid: Schema.number().required(), - label: Schema.string(), - title: Schema.string(), - score: Schema.number().min(0), - balloon: Schema.object({ - color: Schema.string(), - name: Schema.string(), - }), -})).required(); -export function resolveContestProblemJson(text:string, validate = ContestProblemJsonSchema) { - let _problemList = []; - try { - _problemList = validate(JSON.parse(text)); - } catch (e) { - throw new ValidationError('problems'); - } - const problems = [] as ContestProblem[]; - const score = {} as Record; - for (let i = 0; i < _problemList.length; i++) { - const p = _problemList[i]; - if (typeof p.score === 'number' && p.score !== 100) { - score[p.pid] = p.score; - } - problems.push({ - pid: p.pid, - ...(p.score && p.score !== 100 ? { score: p.score } : {}), - ...(p.title && p.title.length > 0 ? { title: p.title } : {}), - ...(p.label && p.label.length > 0 && p.label !== getAlphabeticId(i) ? { label: p.label } : {}), - }); - } - return { - problems, - score, - }; -} - global.Hydro.model.contest = { RULES, buildContestRule, @@ -1193,6 +1093,4 @@ global.Hydro.model.contest = { isExtended, applyProjection, statusText, - resolveContestProblemJson, - ContestProblemJsonSchema, }; diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 0eb1e7ad5f..8a17054b17 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -5,6 +5,7 @@ import yaml from 'js-yaml'; import { ObjectId } from 'mongodb'; import { randomstring, sleep } from '@hydrooj/utils'; +import { ContestProblemConfig } from './interface'; import { buildContent } from './lib/content'; import { Logger } from './logger'; import { PERM, PRIV, STATUS } from './model/builtin'; @@ -623,16 +624,19 @@ export const coreScripts: MigrationScript[] = [ const tdocs = await contest.getMulti(_id, {}).toArray(); for (const tdoc of tdocs) { await contest.edit(_id, tdoc._id, { - problems: tdoc.pids.map((pid, idx) => ({ - pid, - ...(tdoc?.score && tdoc.score[pid] ? { score: tdoc.score[pid] } : {}), - ...(tdoc?.balloon && tdoc.balloon[pid] ? { - balloon: { - name: typeof tdoc.balloon[pid] === 'string' ? '' : tdoc.balloon[pid].name, - color: typeof tdoc.balloon[pid] === 'string' ? tdoc.balloon[pid] : tdoc.balloon[pid].color, - }, - } : {}), - })), + problemConfig: tdoc.pids.reduce((acc, pid) => { + const cp = {} as ContestProblemConfig; + if (tdoc?.score && tdoc.score[pid]) cp.score = tdoc.score[pid]; + if (tdoc?.balloon && tdoc.balloon[pid]) { + if (typeof tdoc.balloon[pid] === 'string') { + cp.balloon = { name: '', color: tdoc.balloon[pid] }; + } else { + cp.balloon = tdoc.balloon[pid]; + } + } + if (Object.keys(cp).length > 0) acc[pid] = cp; + return acc; + }, {}), }); } logger.info('Domain %s done', _id); diff --git a/packages/migrate/scripts/hustoj.ts b/packages/migrate/scripts/hustoj.ts index 9db9ef97ef..7604e38821 100644 --- a/packages/migrate/scripts/hustoj.ts +++ b/packages/migrate/scripts/hustoj.ts @@ -288,7 +288,7 @@ hydrooj install https://hydro.ac/hydroac-client.zip } const tid = await ContestModel.add( domainId, tdoc.title, description || 'Description', - adminUids[0], contestType, tdoc.start_time, endAt, pids, true, + adminUids[0], contestType, tdoc.start_time, endAt, pids, {}, true, { _code: tdoc.password }, ); tidMap[tdoc.contest_id] = tid.toHexString(); diff --git a/packages/migrate/scripts/poj.ts b/packages/migrate/scripts/poj.ts index 295eb8a0a0..5061cd4569 100644 --- a/packages/migrate/scripts/poj.ts +++ b/packages/migrate/scripts/poj.ts @@ -201,7 +201,7 @@ memory: ${pdoc.memory_limit}k`, const endAt = moment(tdoc.end_time).isSameOrBefore(tdoc.start_time) ? moment(tdoc.end_time).add(1, 'minute').toDate() : tdoc.end_time; const tid = await ContestModel.add( domainId, tdoc.title, tdoc.description || 'Description', - adminUids[0], contestType, tdoc.start_time, endAt, pids, true, + adminUids[0], contestType, tdoc.start_time, endAt, pids, {}, true, tdoc.private ? { _code: password } : {}, ); tidMap[tdoc.contest_id] = tid.toHexString(); diff --git a/packages/migrate/scripts/syzoj.ts b/packages/migrate/scripts/syzoj.ts index 61967f8a75..0367f2f80a 100644 --- a/packages/migrate/scripts/syzoj.ts +++ b/packages/migrate/scripts/syzoj.ts @@ -286,7 +286,7 @@ export async function run({ const tid = await ContestModel.add( domainId, tdoc.title, `${tdoc.subtitle ? `#### ${tdoc.subtitle}\n` : ''}${tdoc.information || 'No Description'}`, admin, contentTypeMap[tdoc.type], startAt, endAt, - pids, ratedTids.includes(tdoc.id), { maintainer: tdoc.admins.split('|').map((i) => uidMap[i]), assign: [] }, + pids, {}, ratedTids.includes(tdoc.id), { maintainer: tdoc.admins.split('|').map((i) => uidMap[i]), assign: [] }, ); tidMap[tdoc.id] = tid.toHexString(); } diff --git a/packages/migrate/scripts/universaloj.ts b/packages/migrate/scripts/universaloj.ts index e9f48cdeb1..e90cfa88b4 100644 --- a/packages/migrate/scripts/universaloj.ts +++ b/packages/migrate/scripts/universaloj.ts @@ -324,7 +324,7 @@ export async function run({ const endAt = startAt.clone().add(tdoc.last_min, 'minutes'); const tid = await ContestModel.add( domainId, tdoc.name, content, uidMap[permissions[0]?.username] || 1, info.contest_type?.toLowerCase() || 'oi', - startAt.toDate(), endAt.toDate(), pids, !Object.keys(info).includes('unrated'), { maintainer }, + startAt.toDate(), endAt.toDate(), pids, {}, !Object.keys(info).includes('unrated'), { maintainer }, ); tidMap[tdoc.id] = tid.toHexString(); } diff --git a/packages/onsite-toolkit/index.ts b/packages/onsite-toolkit/index.ts index 2f8c0300bc..d03b16c80e 100644 --- a/packages/onsite-toolkit/index.ts +++ b/packages/onsite-toolkit/index.ts @@ -52,14 +52,14 @@ export function apply(ctx: Context) { const unknownSchool = this.translate('Unknown School'); const submissions = teams.flatMap((i) => { if (!i.journal) return []; - return i.journal.filter((s) => Object.hasOwn(tdoc.pid2idx, s.pid)).map((s) => ({ ...s, uid: i.uid })); + return i.journal.filter((s) => tdoc.pids.includes(s.pid)).map((s) => ({ ...s, uid: i.uid })); }); this.response.body = { payload: { name: tdoc.title, duration: Math.floor((new Date(tdoc.endAt).getTime() - new Date(tdoc.beginAt).getTime()) / 1000), frozen: Math.floor((new Date(tdoc.lockAt).getTime() - new Date(tdoc.beginAt).getTime()) / 1000), - problems: tdoc.problems.map((p, idx) => ({ name: p.label || getAlphabeticId(idx), id: p.pid.toString() })), + problems: tdoc.pids.map((pid, idx) => ({ name: tdoc.problemConfig[pid]?.label || getAlphabeticId(idx), id: pid.toString() })), teams: teams.map((t) => ({ id: t.uid.toString(), name: udict[t.uid].displayName || udict[t.uid].uname, @@ -91,7 +91,7 @@ export function apply(ctx: Context) { type, id: data.id, data, token: `t${token++}`, }); const [pdict, tsdocs] = await Promise.all([ - ProblemModel.getList(tdoc.domainId, tdoc.problems.map((p) => p.pid), true, false, ProblemModel.PROJECTION_LIST, true), + ProblemModel.getList(tdoc.domainId, tdoc.pids, true, false, ProblemModel.PROJECTION_LIST, true), ContestModel.getMultiStatus(tdoc.domainId, { docId: tdoc._id }).toArray(), ]); const udict = await UserModel.getList(tdoc.domainId, tsdocs.map((i) => i.uid)); @@ -192,21 +192,21 @@ export function apply(ctx: Context) { height: 1080, }], })), - ...tdoc.problems.map((p, idx) => getFeed('problems', { - id: `${p.pid}`, - label: String.fromCharCode(65 + idx), - name: p.title || pdict[p.pid].title, + ...tdoc.pids.map((pid, idx) => getFeed('problems', { + id: `${pid}`, + label: tdoc.problemConfig[pid]?.label || getAlphabeticId(idx), + name: pdict[pid].title, ordinal: idx, - color: tdoc.problems[idx]?.balloon?.name || 'white', - rgb: tdoc.problems[idx]?.balloon?.color || '#ffffff', - time_limit: (parseTimeMS((pdict[p.pid].config as ProblemConfig).timeMax) / 1000).toFixed(1), + color: tdoc.problemConfig[pid]?.balloon?.name || 'white', + rgb: tdoc.problemConfig[pid]?.balloon?.color || '#ffffff', + time_limit: (parseTimeMS((pdict[pid].config as ProblemConfig).timeMax) / 1000).toFixed(1), test_data_count: 20, })), ]; let cntJudge = 0; const submissions = tsdocs.flatMap((i) => { if (!i.journal) return []; - const journal = i.journal.filter((s) => Object.hasOwn(tdoc.pid2idx, s.pid)); + const journal = i.journal.filter((s) => tdoc.pids.includes(s.pid)); const result: any[] = []; for (const s of journal) { const submitTime = moment(s.rid.getTimestamp()); diff --git a/packages/scoreboard-xcpcio/index.ts b/packages/scoreboard-xcpcio/index.ts index e68433449c..7ab7c19f07 100644 --- a/packages/scoreboard-xcpcio/index.ts +++ b/packages/scoreboard-xcpcio/index.ts @@ -63,8 +63,8 @@ export async function apply(ctx: Context) { end_time: Math.floor(tdoc.endAt.getTime() / 1000), frozen_time: tdoc.lockAt ? Math.floor((tdoc.endAt.getTime() - tdoc.lockAt.getTime()) / 1000) : 0, penalty: 1200, - problem_quantity: tdoc.problems.length, - problem_id: tdoc.problems.map((cp, idx) => cp.label || getAlphabeticId(idx)), + problem_quantity: tdoc.pids.length, + problem_id: tdoc.pids.map((pid, idx) => tdoc.problemConfig[pid]?.label || getAlphabeticId(idx)), group: { official: '正式队伍', unofficial: '打星队伍', @@ -83,10 +83,10 @@ export async function apply(ctx: Context) { bronze, }, }, - balloon_color: tdoc.problems.some((p) => p.balloon) - ? tdoc.problems.filter((p) => p.balloon).map((p) => ({ + balloon_color: tdoc.pids.some((pid) => tdoc.problemConfig[pid]?.balloon) + ? tdoc.pids.filter((pid) => tdoc.problemConfig[pid]?.balloon).map((pid) => ({ color: '#000', - background_color: p.balloon.color, + background_color: tdoc.problemConfig[pid].balloon.color, })) : [], logo: { @@ -103,7 +103,7 @@ export async function apply(ctx: Context) { const submit = new ObjectId(j.rid as string).getTimestamp().getTime(); const curStatus = status[j.status] || 'SYSTEM_ERROR'; return { - problem_id: tdoc.pid2idx[j.pid], + problem_id: tdoc.pids.indexOf(j.pid), team_id: `${i.uid}`, timestamp: Math.floor(submit - tdoc.beginAt.getTime()), status: realtime diff --git a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx index 4f3f0fab64..af5c7cda57 100644 --- a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx +++ b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx @@ -1,4 +1,5 @@ import { getAlphabeticId } from '@hydrooj/utils/lib/common'; +import { ContestProblemConfig } from 'hydrooj/src/interface'; import { debounce } from 'lodash'; import React from 'react'; import { DndProvider, useDrag, useDrop } from 'react-dnd'; @@ -6,21 +7,15 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import { api, i18n } from 'vj/utils'; import ProblemSelectAutoComplete from '../autocomplete/components/ProblemSelectAutoComplete'; -export interface Problem { +interface Problem extends ContestProblemConfig { pid: number; - label?: string; - title?: string; - score?: number; - balloon?: { - color: string; - name: string; - }; _tmpId?: string; // use this as key } export interface ContestProblemEditorProps { - problems: Problem[]; - onChange: (problems: Problem[]) => void; + pids: number[]; + problemConfig: ContestProblemConfig[]; + onChange: (pids: number[], problemConfig: Record) => void; } const randomId = () => Math.random().toString(16).substring(2); const ItemTypes = { @@ -57,8 +52,10 @@ const DraggableRow = ({ }); return ( -
+ + { problemRefs.current[index] = ref; }} @@ -67,15 +64,6 @@ const DraggableRow = ({ /> {problemRawTitles[problem.pid]} - handleChange(index, 'title', e.target.value)} - placeholder={i18n('(leave blank if none)')} - /> - { +const ContestProblemEditor = ({ pids: initialPids, problemConfig: initialProblemConfig, onChange: _onChange }: ContestProblemEditorProps) => { // TODO: also support balloon and other fields in the future - const [problems, setProblems] = React.useState(initialProblems.map((el, idx) => ({ - ...el, - _tmpId: randomId(), - ...(!el.label ? { label: getAlphabeticId(idx) } : {}), - }))); + const [problems, setProblems] = React.useState(initialPids.map((pid, idx) => { + const cp = initialProblemConfig[pid] || {}; + return { + pid, + ...cp, + ...(!cp.label ? { label: getAlphabeticId(idx) } : {}), + _tmpId: randomId(), + }; + })); const problemRefs = React.useRef<{ [key: number]: any }>({}); const [problemRawTitles, setProblemRawTitles] = React.useState>({}); @@ -133,19 +125,20 @@ const ContestProblemEditor = ({ problems: initialProblems, onChange: _onChange } const onChange = (newProblems: Problem[]) => { const fixedProblems = newProblems.map((i) => { const problem = { ...i }; - // undefined is ok, JSON.stringify will ignore it - if (problem.title === '') problem.title = undefined; if (problem.score === 100) problem.score = undefined; return problem; }); setProblems(fixedProblems); - _onChange(fixedProblems.map((i, idx) => { - const { _tmpId, label, ...p } = i; - return { - ...p, - ...(label !== getAlphabeticId(idx) ? { label } : {}), - }; - })); + + const pids = fixedProblems.map((i) => i.pid); + const problemConfig = fixedProblems.reduce((acc, cur, idx) => { + const cp = {} as ContestProblemConfig; + if (cur.label && cur.label !== getAlphabeticId(idx)) cp.label = cur.label; + if (cur.score && cur.score !== 100) cp.score = cur.score; + if (Object.keys(cur).length > 0) acc[cur.pid] = cp; + return acc; + }, {}); + _onChange(pids, problemConfig); }; const handleAdd = () => { @@ -180,13 +173,11 @@ const ContestProblemEditor = ({ problems: initialProblems, onChange: _onChange } onChange(newProblems); }; - const moveRow = (iX: number, iY: number) => { + const moveRow = (dragIndex: number, hoverIndex: number) => { const newProblems = [...problems]; - [newProblems[iX], newProblems[iY]] = [newProblems[iY], newProblems[iX]]; - const [labelX, labelY] = [newProblems[iX].label, newProblems[iY].label]; - newProblems[iX].label = labelY; - newProblems[iY].label = labelX; + const [movedItem] = newProblems.splice(dragIndex, 1); + newProblems.splice(hoverIndex, 0, movedItem); setProblems(newProblems); onChange(newProblems); @@ -200,8 +191,7 @@ const ContestProblemEditor = ({ problems: initialProblems, onChange: _onChange }
pid{i18n('Raw Title')}{i18n('Custom Title')}{i18n('Title')} {i18n('Label')} {i18n('Score')} {i18n('Action')}
{now === pid - ? ({cp.label || getAlphabeticId(idx)}) - : ({cp.label || getAlphabeticId(idx)})} + ? ({cp?.label || getAlphabeticId(idx)}) + : ({cp?.label || getAlphabeticId(idx)})} { - ProblemSelectAutoComplete.getOrConstruct($('[name="pids"]'), { multi: true, clearDefaultValue: false }); UserSelectAutoComplete.getOrConstruct($('[name="maintainer"]'), { multi: true, clearDefaultValue: false }); LanguageSelectAutoComplete.getOrConstruct($('[name=langs]'), { multi: true }); if ($('#problem-editor').length) { - const problemsInput = $('[name=problems]'); + const pidsInput = $('[name=pids]'); + const problemConfigInput = $('[name=problemConfig]'); ReactDOM.createRoot($('#problem-editor')[0]).render( { - problemsInput.val(JSON.stringify(problems)); + pids={pidsInput.val().toString().split(',').map((i) => +i)} + problemConfig={JSON.parse(problemConfigInput.val() as string)} + onChange={(pids, problemConfig) => { + pidsInput.val(pids.join(',')); + problemConfigInput.val(JSON.stringify(problemConfig)); }} />, ); diff --git a/packages/ui-default/templates/components/contest.html b/packages/ui-default/templates/components/contest.html index fcc639ee46..a5fdf42882 100644 --- a/packages/ui-default/templates/components/contest.html +++ b/packages/ui-default/templates/components/contest.html @@ -8,5 +8,5 @@ {% endmacro %} {% macro render_clarification_subject(tdoc, pdict, subject) %} -{% if subject == 0 %}{{ _('General Issue') }}{% elif subject == -1 %}{{ _('Technical Issue') }}{% else %}{% set cp = utils.getContestProblemConfig(subject, tdoc) %}{{ cp.label }}. {{ cp.title|default(pdict[subject].title) }}{% endif %} +{% if subject == 0 %}{{ _('General Issue') }}{% elif subject == -1 %}{{ _('Technical Issue') }}{% else %}{{ utils.getContestProblemConfig(subject, tdoc).label }}. {{ pdict[subject].title }}{% endif %} {% endmacro %} \ No newline at end of file diff --git a/packages/ui-default/templates/contest_balloon.html b/packages/ui-default/templates/contest_balloon.html index 26f2d2de6c..1671d27f13 100644 --- a/packages/ui-default/templates/contest_balloon.html +++ b/packages/ui-default/templates/contest_balloon.html @@ -13,7 +13,7 @@

{{ _('Balloon Status') }}

{{ noscript_note.render() }}
- {% if tdoc.problems|length != 0 and not tdoc.problems[0].balloon %} + {% if tdoc.pids|length != 0 and not utils.getContestProblemConfig(tdoc.pids[0], tdoc).balloon %} {{ nothing.render('Please set the balloon color for each problem first.') }} {% else %} diff --git a/packages/ui-default/templates/contest_edit.html b/packages/ui-default/templates/contest_edit.html index 5bdc1f3ca0..23eeff81cf 100644 --- a/packages/ui-default/templates/contest_edit.html +++ b/packages/ui-default/templates/contest_edit.html @@ -67,7 +67,8 @@

{{ _('Basic Info') }}

- + +
diff --git a/packages/ui-default/templates/contest_manage.html b/packages/ui-default/templates/contest_manage.html index 8cc0a091fe..5dada0495c 100644 --- a/packages/ui-default/templates/contest_manage.html +++ b/packages/ui-default/templates/contest_manage.html @@ -21,15 +21,16 @@ - {%- for cp in tdoc.problems -%} + {%- for pid in tdoc.pids -%} + {% set cp = utils.getContestProblemConfig(pid, tdoc) %} {%- endfor -%} @@ -126,8 +127,8 @@

{{ _('Send Broadcast Message') }}

diff --git a/packages/ui-default/templates/contest_problemlist.html b/packages/ui-default/templates/contest_problemlist.html index 4503445457..b9cf9f0c7e 100644 --- a/packages/ui-default/templates/contest_problemlist.html +++ b/packages/ui-default/templates/contest_problemlist.html @@ -41,8 +41,8 @@

{{ _('Problems') }}

- {%- for cp in tdoc.problems -%} - {% set pid = cp.pid %} + {%- for pid in tdoc.pids -%} + {% set cp = utils.getContestProblemConfig(pid, tdoc) %} {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} {% if psdict[pid] and psdict[pid].rid %} @@ -185,8 +185,9 @@

{{ _('Send Clarification Request') }}

diff --git a/packages/ui-default/templates/homework_detail.html b/packages/ui-default/templates/homework_detail.html index f34b8f4be6..a9009bb34f 100644 --- a/packages/ui-default/templates/homework_detail.html +++ b/packages/ui-default/templates/homework_detail.html @@ -40,8 +40,7 @@

{{ _('Problem') }}

{% set isAdmin = handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD) %} {% set ntdoc = model.contest.isDone(tdoc) or (tsdoc.attend and not model.contest.isNotStarted(tdoc)) %} - {%- for cp in tdoc.problems -%} - {% set pid = cp.pid %} + {%- for pid in tdoc.pids -%} {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} {% if psdict[pid] and psdict[pid].rid %} diff --git a/packages/ui-default/templates/homework_edit.html b/packages/ui-default/templates/homework_edit.html index 602a484e01..a8a1acb2ed 100644 --- a/packages/ui-default/templates/homework_edit.html +++ b/packages/ui-default/templates/homework_edit.html @@ -87,8 +87,9 @@
-
- +
+ +
- - {{ problem.render_problem_title(pdict[cp.pid], tdoc=tdoc, invalid=true, alphabetic=true) }} + + {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, invalid=true, alphabetic=true) }} - {{ cp.score|default(100) }} + {{ cp.score|default(100) }}
diff --git a/packages/ui-default/templates/partials/contest_sidebar.html b/packages/ui-default/templates/partials/contest_sidebar.html index 11550082d9..e541ee3bb6 100644 --- a/packages/ui-default/templates/partials/contest_sidebar.html +++ b/packages/ui-default/templates/partials/contest_sidebar.html @@ -117,7 +117,7 @@

{{ tdoc.title }}

{% endif %}
{{ _('Rule') }}
{{ _(model.contest.RULES[tdoc.rule].TEXT) }}
-
{{ _('Problem') }}
{{ tdoc.problems.length }}
+
{{ _('Problem') }}
{{ tdoc.pids.length }}
{{ _('Start at') }}
{{ contest.render_time(tsdoc.startAt or tdoc.beginAt) }}
{{ _('End at') }}
{{ contest.render_time(tsdoc.endAt or tdoc.endAt) }}
{{ _('Duration') }}
{{ contest.render_duration(tdoc) }} {{ _('hour(s)') }}
diff --git a/packages/ui-default/templates/partials/contest_sidebar_management.html b/packages/ui-default/templates/partials/contest_sidebar_management.html index 2372be175c..1114449f35 100644 --- a/packages/ui-default/templates/partials/contest_sidebar_management.html +++ b/packages/ui-default/templates/partials/contest_sidebar_management.html @@ -42,7 +42,7 @@

{{ tdoc.title }}

{% endif %}
{{ _('Rule') }}
{{ model.contest.RULES[tdoc.rule].TEXT }}
-
{{ _('Problem') }}
{{ tdoc.problems.length }}
+
{{ _('Problem') }}
{{ tdoc.pids.length }}
{{ _('Start at') }}
{{ contest.render_time(tsdoc.startAt or tdoc.beginAt) }}
{{ _('End at') }}
{{ contest.render_time(tsdoc.endAt or tdoc.endAt) }}
{{ _('Duration') }}
{{ contest.render_duration(tdoc) }} {{ _('hour(s)') }}
diff --git a/packages/ui-default/templates/problem_detail.html b/packages/ui-default/templates/problem_detail.html index a7e1bd37a1..11aa9d537f 100644 --- a/packages/ui-default/templates/problem_detail.html +++ b/packages/ui-default/templates/problem_detail.html @@ -71,17 +71,17 @@

#{{ pdoc.pid|default(pdoc.docId) }} {%- endif -%}. {%- if tdoc -%}{{ utils.getContestProblemConfig(pdoc.docId, tdoc).title|default(pdoc.title) }}{%- else -%}{{ pdoc.title }}{%- endif -%}

- {% if tdoc and tdoc.problems|length <= 26 %} + {% if tdoc and tdoc.pids|length <= 26 %}
- {% for cp in tdoc.problems -%} - {% set pid = cp.pid %} + {% for pid in tdoc.pids -%} + {% set cp = utils.getContestProblemConfig(pid, tdoc) %} {% set status = tsdoc.detail[pid].status %} {% set pass = status == STATUS.STATUS_ACCEPTED %} {% set fail = status and not pass %} - {{ cp.label|default(utils.getAlphabeticId(tdoc.pid2idx[cp.pid])) }} + {{ cp.label }} {% if status %}{% endif %} {%- endfor %} diff --git a/packages/utils/lib/common.ts b/packages/utils/lib/common.ts index 85d300745c..5a746f9049 100644 --- a/packages/utils/lib/common.ts +++ b/packages/utils/lib/common.ts @@ -247,11 +247,11 @@ export const getAlphabeticId = (() => { })(); export const getContestProblemConfig = (pid, tdoc) => { - const idx = tdoc.pid2idx?.[pid]; - const cp = tdoc.problems[idx]; + const cp = tdoc.problemConfig[pid] || {}; + const idx = tdoc.pids.indexOf(pid); // create label if not exists here will simplify the logic on template return { ...cp, - label: cp.label || (idx === undefined ? '' : getAlphabeticId(idx)), + label: cp?.label || getAlphabeticId(idx), }; }; From ed21dcd538cba260f6451cbe7abebb900dba7dff Mon Sep 17 00:00:00 2001 From: Bhscer Date: Mon, 21 Jul 2025 18:46:28 +0800 Subject: [PATCH 25/27] fix --- packages/hydrooj/src/handler/homework.ts | 2 +- packages/hydrooj/src/handler/problem.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hydrooj/src/handler/homework.ts b/packages/hydrooj/src/handler/homework.ts index a01f699b5b..ce3428e693 100644 --- a/packages/hydrooj/src/handler/homework.ts +++ b/packages/hydrooj/src/handler/homework.ts @@ -174,7 +174,7 @@ class HomeworkEditHandler extends Handler { extensionDays, penaltyRules: tid ? yaml.dump(tdoc.penaltyRules) : null, pids: tid ? tdoc.pids.join(',') : '', - problemConfig: tid ? tdoc.problemConfig : [], + problemConfig: tid ? tdoc.problemConfig : {}, page_name: tid ? 'homework_edit' : 'homework_create', }; } diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 20bc3b5324..120b9aaaa5 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -305,7 +305,7 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler { if (this.pdoc.config.langs) t.push(this.pdoc.config.langs); if (ddoc.langs) t.push(ddoc.langs.split(',').map((i) => i.trim()).filter((i) => i)); if (this.domain.langs) t.push(this.domain.langs.split(',').map((i) => i.trim()).filter((i) => i)); - if (this.tdoc?.langs && this.tdoc?.langs.length) t.push(this.tdoc.langs); + if (this.tdoc?.langs?.length) t.push(this.tdoc.langs); if (this.pdoc.config.type === 'remote_judge') { const p = this.pdoc.config.subType; const dl = Object.keys(setting.langs).filter((i) => i.startsWith(`${p}.`) || setting.langs[i].validAs[p]); From 06f8a126b63cf0c2c08016e651fe6e8556cd6cfc Mon Sep 17 00:00:00 2001 From: Bhscer Date: Mon, 21 Jul 2025 18:48:58 +0800 Subject: [PATCH 26/27] fix --- .../components/contestProblemEditor/ContestProblemEditor.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx index af5c7cda57..24d128ad5a 100644 --- a/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx +++ b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx @@ -14,7 +14,7 @@ interface Problem extends ContestProblemConfig { export interface ContestProblemEditorProps { pids: number[]; - problemConfig: ContestProblemConfig[]; + problemConfig: Record; onChange: (pids: number[], problemConfig: Record) => void; } const randomId = () => Math.random().toString(16).substring(2); @@ -135,7 +135,7 @@ const ContestProblemEditor = ({ pids: initialPids, problemConfig: initialProblem const cp = {} as ContestProblemConfig; if (cur.label && cur.label !== getAlphabeticId(idx)) cp.label = cur.label; if (cur.score && cur.score !== 100) cp.score = cur.score; - if (Object.keys(cur).length > 0) acc[cur.pid] = cp; + if (Object.keys(cp).length > 0) acc[cur.pid] = cp; return acc; }, {}); _onChange(pids, problemConfig); From c56b650cc9292876988689f25c3f49dbdeea26a1 Mon Sep 17 00:00:00 2001 From: Bhscer Date: Mon, 21 Jul 2025 19:15:47 +0800 Subject: [PATCH 27/27] let problemConfig in data instead in param --- packages/hydrooj/src/handler/contest.ts | 4 ++-- packages/hydrooj/src/handler/homework.ts | 4 ++-- packages/hydrooj/src/model/contest.ts | 9 +++++---- packages/migrate/scripts/hustoj.ts | 2 +- packages/migrate/scripts/poj.ts | 2 +- packages/migrate/scripts/syzoj.ts | 2 +- packages/migrate/scripts/universaloj.ts | 2 +- 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 59a821ff69..21726028e1 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -341,8 +341,8 @@ export class ContestEditHandler extends Handler { } } else { tid = await contest.add( - domainId, title, content, this.user._id, rule, beginAt, endAt, pids, problemConfig, rated, - { duration: contestDuration }, + domainId, title, content, this.user._id, rule, beginAt, endAt, pids, rated, + { duration: contestDuration, problemConfig }, ); } const task = { diff --git a/packages/hydrooj/src/handler/homework.ts b/packages/hydrooj/src/handler/homework.ts index ce3428e693..3f5b6157e2 100644 --- a/packages/hydrooj/src/handler/homework.ts +++ b/packages/hydrooj/src/handler/homework.ts @@ -221,9 +221,9 @@ class HomeworkEditHandler extends Handler { await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id, true); if (!tid) { tid = await contest.add(domainId, title, content, this.user._id, - 'homework', beginAt.toDate(), endAt.toDate(), pids, problemConfig, rated, + 'homework', beginAt.toDate(), endAt.toDate(), pids, rated, { - penaltySince: penaltySince.toDate(), penaltyRules, assign, + penaltySince: penaltySince.toDate(), penaltyRules, assign, problemConfig, }); } else { await contest.edit(domainId, tid, { diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index 363817a518..85e45e765c 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -8,7 +8,7 @@ import { ContestScoreboardHiddenError, ValidationError, } from '../error'; import { - BaseUserDict, ContestProblemConfig, ContestRule, ContestRules, ProblemDict, RecordDoc, + BaseUserDict, ContestRule, ContestRules, ProblemDict, RecordDoc, ScoreboardConfig, ScoreboardNode, ScoreboardRow, SubtaskResult, Tdoc, } from '../interface'; import bus from '../service/bus'; @@ -798,17 +798,18 @@ function _getStatusJournal(tsdoc) { export async function add( domainId: string, title: string, content: string, owner: number, rule: string, beginAt = new Date(), endAt = new Date(), - pids: number[] = [], problemConfig: Record = {}, rated = false, data: Partial = {}, + pids: number[] = [], rated = false, data: Partial = {}, ) { if (!RULES[rule]) throw new ValidationError('rule'); if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt'); + data.problemConfig ||= {}; Object.assign(data, { - content, owner, title, rule, beginAt, endAt, pids, problemConfig, attend: 0, + content, owner, title, rule, beginAt, endAt, pids, attend: 0, }); RULES[rule].check(data); await bus.parallel('contest/before-add', data); const docId = await document.add(domainId, content, owner, document.TYPE_CONTEST, null, null, null, { - assign: [], ...data, title, rule, beginAt, endAt, pids, problemConfig, attend: 0, rated, + assign: [], ...data, title, rule, beginAt, endAt, pids, attend: 0, rated, }); await bus.parallel('contest/add', data, docId); return docId; diff --git a/packages/migrate/scripts/hustoj.ts b/packages/migrate/scripts/hustoj.ts index 7604e38821..9db9ef97ef 100644 --- a/packages/migrate/scripts/hustoj.ts +++ b/packages/migrate/scripts/hustoj.ts @@ -288,7 +288,7 @@ hydrooj install https://hydro.ac/hydroac-client.zip } const tid = await ContestModel.add( domainId, tdoc.title, description || 'Description', - adminUids[0], contestType, tdoc.start_time, endAt, pids, {}, true, + adminUids[0], contestType, tdoc.start_time, endAt, pids, true, { _code: tdoc.password }, ); tidMap[tdoc.contest_id] = tid.toHexString(); diff --git a/packages/migrate/scripts/poj.ts b/packages/migrate/scripts/poj.ts index 5061cd4569..295eb8a0a0 100644 --- a/packages/migrate/scripts/poj.ts +++ b/packages/migrate/scripts/poj.ts @@ -201,7 +201,7 @@ memory: ${pdoc.memory_limit}k`, const endAt = moment(tdoc.end_time).isSameOrBefore(tdoc.start_time) ? moment(tdoc.end_time).add(1, 'minute').toDate() : tdoc.end_time; const tid = await ContestModel.add( domainId, tdoc.title, tdoc.description || 'Description', - adminUids[0], contestType, tdoc.start_time, endAt, pids, {}, true, + adminUids[0], contestType, tdoc.start_time, endAt, pids, true, tdoc.private ? { _code: password } : {}, ); tidMap[tdoc.contest_id] = tid.toHexString(); diff --git a/packages/migrate/scripts/syzoj.ts b/packages/migrate/scripts/syzoj.ts index 0367f2f80a..61967f8a75 100644 --- a/packages/migrate/scripts/syzoj.ts +++ b/packages/migrate/scripts/syzoj.ts @@ -286,7 +286,7 @@ export async function run({ const tid = await ContestModel.add( domainId, tdoc.title, `${tdoc.subtitle ? `#### ${tdoc.subtitle}\n` : ''}${tdoc.information || 'No Description'}`, admin, contentTypeMap[tdoc.type], startAt, endAt, - pids, {}, ratedTids.includes(tdoc.id), { maintainer: tdoc.admins.split('|').map((i) => uidMap[i]), assign: [] }, + pids, ratedTids.includes(tdoc.id), { maintainer: tdoc.admins.split('|').map((i) => uidMap[i]), assign: [] }, ); tidMap[tdoc.id] = tid.toHexString(); } diff --git a/packages/migrate/scripts/universaloj.ts b/packages/migrate/scripts/universaloj.ts index e90cfa88b4..e9f48cdeb1 100644 --- a/packages/migrate/scripts/universaloj.ts +++ b/packages/migrate/scripts/universaloj.ts @@ -324,7 +324,7 @@ export async function run({ const endAt = startAt.clone().add(tdoc.last_min, 'minutes'); const tid = await ContestModel.add( domainId, tdoc.name, content, uidMap[permissions[0]?.username] || 1, info.contest_type?.toLowerCase() || 'oi', - startAt.toDate(), endAt.toDate(), pids, {}, !Object.keys(info).includes('unrated'), { maintainer }, + startAt.toDate(), endAt.toDate(), pids, !Object.keys(info).includes('unrated'), { maintainer }, ); tidMap[tdoc.id] = tid.toHexString(); }