Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0ece60b
a tmp commit
bhscer Jul 16, 2025
c6d2002
small fix
bhscer Jul 16, 2025
2355c1f
fix the problem of dnd
bhscer Jul 16, 2025
74a181b
add a record column to optimize the speed
bhscer Jul 17, 2025
9855dbd
remove some needless checks
bhscer Jul 17, 2025
137c186
fix
bhscer Jul 17, 2025
2a15f6b
fix
bhscer Jul 17, 2025
c5ab8cc
optimze
bhscer Jul 17, 2025
2ff5a01
fix
bhscer Jul 17, 2025
2dc6608
fix dep in ui-default
bhscer Jul 17, 2025
6f455cf
fix some missing label
bhscer Jul 17, 2025
cce8108
label is no need when upgrade
bhscer Jul 17, 2025
8d661b5
lagacy add/edit contest pids need no label
bhscer Jul 17, 2025
c9772e3
add deprecated flag && fix
bhscer Jul 18, 2025
021e0b9
admin view homework problem need no tid param
bhscer Jul 18, 2025
5262ee3
support restore problem when back from the ValidationError page
bhscer Jul 18, 2025
956c4b8
fix recalcStatus missing && optimize the scoreboard with large data
bhscer Jul 18, 2025
5d55662
fix the judgement of a scoreboard is large or not
bhscer Jul 18, 2025
418b4be
fix a missing judge of tooltip
bhscer Jul 18, 2025
d63c2a9
fix some missing usage of pid2idx
bhscer Jul 18, 2025
16cc94b
fix missing label
bhscer Jul 18, 2025
cc7b8b7
fix existed problems in review
bhscer Jul 18, 2025
6bc4c51
Merge branch 'master' into better-contest
bhscer Jul 20, 2025
e55f1d3
fix
bhscer Jul 21, 2025
9584fd8
strcuture optimize
bhscer Jul 21, 2025
ed21dcd
fix
bhscer Jul 21, 2025
06f8a12
fix
bhscer Jul 21, 2025
c56b650
let problemConfig in data instead in param
bhscer Jul 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/hydrooj/locales/zh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: 附加文件
Expand Down Expand Up @@ -211,6 +212,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} 的数据未找到。
Expand Down Expand Up @@ -435,6 +437,7 @@ Judged At: 评测时间
Judged By: 评测机
Judging Queue: 评测队列
Keep current expiration: 保持当前过期设置
Label: 标号
Language: 语言
last active at: 最后活动于
last login at: 最后登录于
Expand Down Expand Up @@ -638,6 +641,7 @@ Quote: 引用
Rank: 排名
ranking: 排名
Ranking: 排名
Raw Title: 原始标题
Read record codes after accept: 题目通过后读取记录的代码
Recalculates nSubmit and nAccept in problem status.: 重新计算每道题目的 AC 量和提交量
Recommended: 推荐
Expand Down
72 changes: 44 additions & 28 deletions packages/hydrooj/src/handler/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +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 cp = this.tdoc.problems[this.tdoc.pid2idx[pdoc.docId]];
this.response.body.overrideNav = [
{
name: 'contest_main',
Expand All @@ -132,7 +133,7 @@ export class ContestDetailBaseHandler extends Handler {
},
{
name: 'problem_detail',
displayName: `${getAlphabeticId(this.tdoc.pids.indexOf(pdoc.docId))}. ${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,
},
Expand Down Expand Up @@ -188,15 +189,15 @@ 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),
]);
this.response.body = {
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() });
Expand All @@ -209,7 +210,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];
}
Expand Down Expand Up @@ -245,7 +246,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.pid2idx[subject] + 1}` : 'the contest'],
url: this.url('contest_manage', { tid }),
}), message.FLAG_I18N | message.FLAG_UNREAD);
}
Expand Down Expand Up @@ -283,7 +284,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',
};
Expand All @@ -296,7 +297,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)
Expand All @@ -308,12 +309,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();
Expand All @@ -324,15 +326,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,
Expand Down Expand Up @@ -438,7 +443,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),
Expand Down Expand Up @@ -516,10 +521,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.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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not store redundant data in db

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should update score since some old plugins may still using this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is intented to drop legacy apis in beta versions. if you keep this now, it means we have to keep supporting this thing until v6

(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();
}
Expand Down Expand Up @@ -593,7 +605,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'] : []),
};
Expand All @@ -607,11 +619,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();
}

Expand Down Expand Up @@ -745,7 +759,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 }));
}
}
Expand All @@ -770,9 +784,10 @@ export async function apply(ctx: Context) {
const page_name = tdoc.rule === 'homework'
? 'homework_scoreboard'
: 'contest_scoreboard';
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,
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';
Expand All @@ -785,14 +800,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<number, number> = {};
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 = {
Expand All @@ -804,22 +818,24 @@ 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 journal = i.journal.filter((s) => Object.hasOwn(tdoc.pid2idx, s.pid));
const c = Counter();
return journal.map((s) => {
const id = pid(tdoc.pids.indexOf(s.pid));
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'}`;
});
});
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, 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;
Expand Down
21 changes: 13 additions & 8 deletions packages/hydrooj/src/handler/homework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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',
};
}
Expand All @@ -187,18 +187,19 @@ 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)
@param('langs', Types.CommaSeperatedArray, true)
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);
Expand All @@ -214,14 +215,18 @@ 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,
content,
beginAt: beginAt.toDate(),
endAt: endAt.toDate(),
pids,
problems,
score,
penaltySince: penaltySince.toDate(),
penaltyRules,
rated,
Expand All @@ -232,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);
}
}
Expand Down
Loading
Loading