Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
45 changes: 29 additions & 16 deletions packages/hydrooj/src/handler/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -132,7 +132,7 @@ export class ContestDetailBaseHandler extends Handler {
},
{
name: 'problem_detail',
displayName: `${getAlphabeticId(this.tdoc.pids.indexOf(pdoc.docId))}. ${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,
},
Expand Down Expand Up @@ -196,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 = Object.values(this.tdoc.score || {}).some((i) => i && i !== 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() });
Expand Down Expand Up @@ -284,6 +284,7 @@ export class ContestEditHandler extends Handler {
tdoc: this.tdoc,
duration: tid ? -beginAt.diff(this.tdoc.endAt, 'hour', true) : 2,
pids: tid ? this.tdoc.pids.join(',') : '',
problemConfig: tid ? this.tdoc.problemConfig : {},
beginAt,
page_name: tid ? 'contest_edit' : 'contest_create',
};
Expand All @@ -297,6 +298,7 @@ export class ContestEditHandler extends Handler {
@param('content', Types.Content)
@param('rule', Types.Range(Object.keys(contest.RULES).filter((i) => !contest.RULES[i].hidden)))
@param('pids', Types.Content)
@param('problemConfig', Types.Content)
@param('rated', Types.Boolean)
@param('code', Types.String, true)
@param('autoHide', Types.Boolean)
Expand All @@ -308,11 +310,17 @@ 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, _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);
let problemConfig = {} as Record<number, ContestProblemConfig>;
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');
Expand All @@ -324,15 +332,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, problemConfig, rated, duration: contestDuration,
});
if (this.tdoc.beginAt !== beginAt || this.tdoc.endAt !== endAt
|| 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 });
tid = await contest.add(
domainId, title, content, this.user._id, rule, beginAt, endAt, pids, rated,
{ duration: contestDuration, problemConfig },
);
}
const task = {
type: 'schedule', subType: 'contest', domainId, tid,
Expand Down Expand Up @@ -517,9 +528,9 @@ export class ContestManagementHandler extends ContestManagementBaseHandler {
@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 });
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();
}
Expand Down Expand Up @@ -606,12 +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 (const pid of this.tdoc.pids) {
if (!config[pid]) throw new ValidationError('color');
balloon[pid] = config[pid.toString()];
this.tdoc.problemConfig[pid] ||= {};
this.tdoc.problemConfig[pid].balloon = config[pid.toString()];
}
await contest.edit(domainId, tid, { balloon });
await contest.edit(domainId, tid, { problemConfig: this.tdoc.problemConfig });
this.back();
}

Expand Down Expand Up @@ -770,9 +781,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 @@ -792,7 +804,6 @@ export async function apply(ctx: Context) {
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 @@ -807,7 +818,7 @@ export async function apply(ctx: Context) {
const journal = i.journal.filter((s) => tdoc.pids.includes(s.pid));
const c = Counter();
return journal.map((s) => {
const id = pid(tdoc.pids.indexOf(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'}`;
});
Expand All @@ -819,7 +830,9 @@ export async function apply(ctx: Context) {
`@teams ${tdoc.attend}`,
`@submissions ${submissions.length}`,
].concat(
tdoc.pids.map((i, idx) => `@p ${pid(idx)},${escape(pdict[i]?.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
? udict[i.uid].displayName : udict[i.uid].uname;
Expand Down
21 changes: 16 additions & 5 deletions packages/hydrooj/src/handler/homework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ 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';
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';
Expand Down Expand Up @@ -174,6 +174,7 @@ class HomeworkEditHandler extends Handler {
extensionDays,
penaltyRules: tid ? yaml.dump(tdoc.penaltyRules) : null,
pids: tid ? tdoc.pids.join(',') : '',
problemConfig: tid ? tdoc.problemConfig : {},
page_name: tid ? 'homework_edit' : 'homework_create',
};
}
Expand All @@ -188,16 +189,23 @@ class HomeworkEditHandler extends Handler {
@param('title', Types.Title)
@param('content', Types.Content)
@param('pids', Types.Content)
@param('problemConfig', 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, _pids: string, _problemConfig: string, rated = false,
maintainer: number[] = [], assign: string[] = [], langs: string[] = [],
) {
let problemConfig = {} as Record<number, ContestProblemConfig>;
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);
Expand All @@ -214,14 +222,17 @@ 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, problemConfig,
});
} else {
await contest.edit(domainId, tid, {
title,
content,
beginAt: beginAt.toDate(),
endAt: endAt.toDate(),
pids,
problemConfig,
penaltySince: penaltySince.toDate(),
penaltyRules,
rated,
Expand All @@ -232,7 +243,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.pids, pids)) {
await contest.recalcStatus(domainId, tdoc.docId);
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/hydrooj/src/handler/problem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) 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]);
Expand Down
17 changes: 12 additions & 5 deletions packages/hydrooj/src/handler/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<RecordDoc> = { contest: tid };
Expand Down Expand Up @@ -68,9 +68,15 @@ 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 pidOrLabel = 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.pids.find((_pid, idx) => {
if (tdoc.problemConfig[_pid]?.label) return tdoc.problemConfig[_pid]?.label === pid;
return pid === getAlphabeticId(idx);
});
if (result) pid = result;
}
const pdoc = await problem.get(domainId, pid);
if (pdoc) q.pid = pdoc.docId;
Expand Down Expand Up @@ -114,7 +120,7 @@ class RecordListHandler extends ContestDetailBaseHandler {
udict,
all,
allDomain,
filterPid: pid,
filterPid: pidOrLabel,
filterTid: tid,
filterUidOrName: uidOrName,
filterLang: lang,
Expand Down Expand Up @@ -301,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;
Expand Down
10 changes: 10 additions & 0 deletions packages/hydrooj/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,13 @@ export interface TrainingNode {
pids: number[],
}

export interface ContestProblemConfig {
label?: string;
score?: number;
balloon?: { color: string, name: string };
// [key: string]: any,
}

export interface Tdoc extends Document {
docId: ObjectId;
docType: document['TYPE_CONTEST'];
Expand All @@ -257,6 +264,7 @@ export interface Tdoc extends Document {
content: string;
rule: string;
pids: number[];
problemConfig: Record<number, ContestProblemConfig>;
rated?: boolean;
_code?: string;
assign?: string[];
Expand All @@ -267,7 +275,9 @@ export interface Tdoc extends Document {
lockAt?: Date;
unlocked?: boolean;
autoHide?: boolean;
/** @deprecated */
balloon?: Record<number, string | { color: string, name: string }>;
/** @deprecated */
score?: Record<number, number>;
langs?: string[];

Expand Down
Loading
Loading