diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index 988e63af5e..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: 附加文件 @@ -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} 的数据未找到。 @@ -435,6 +437,7 @@ Judged At: 评测时间 Judged By: 评测机 Judging Queue: 评测队列 Keep current expiration: 保持当前过期设置 +Label: 标号 Language: 语言 last active at: 最后活动于 last login at: 最后登录于 @@ -638,6 +641,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..21726028e1 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'; @@ -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, }, @@ -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() }); @@ -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', }; @@ -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) @@ -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; + 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'); @@ -324,7 +332,7 @@ 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 @@ -332,7 +340,10 @@ export class ContestEditHandler extends Handler { 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, @@ -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(); } @@ -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(); } @@ -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'; @@ -792,7 +804,6 @@ export async 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) => getAlphabeticId(i); const escape = (i: string) => i.replace(/[",]/g, ''); const unknownSchool = this.translate('Unknown School'); const statusMap = { @@ -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'}`; }); @@ -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; diff --git a/packages/hydrooj/src/handler/homework.ts b/packages/hydrooj/src/handler/homework.ts index 033708717b..3f5b6157e2 100644 --- a/packages/hydrooj/src/handler/homework.ts +++ b/packages/hydrooj/src/handler/homework.ts @@ -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'; @@ -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', }; } @@ -188,6 +189,7 @@ 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) @@ -195,9 +197,15 @@ 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, _pids: string, _problemConfig: string, rated = false, maintainer: number[] = [], assign: string[] = [], langs: string[] = [], ) { + 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); @@ -214,7 +222,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, problemConfig, + }); } else { await contest.edit(domainId, tid, { title, @@ -222,6 +232,7 @@ class HomeworkEditHandler extends Handler { beginAt: beginAt.toDate(), endAt: endAt.toDate(), pids, + problemConfig, penaltySince: penaltySince.toDate(), penaltyRules, rated, @@ -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); } } diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index beca5b8203..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) 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]); diff --git a/packages/hydrooj/src/handler/record.ts b/packages/hydrooj/src/handler/record.ts index 264e80cfda..9a609d3ff1 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'; @@ -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 }; @@ -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; @@ -114,7 +120,7 @@ class RecordListHandler extends ContestDetailBaseHandler { udict, all, allDomain, - filterPid: pid, + filterPid: pidOrLabel, filterTid: tid, filterUidOrName: uidOrName, filterLang: lang, @@ -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; diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index de35a43f09..5ec742738b 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -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']; @@ -257,6 +264,7 @@ export interface Tdoc extends Document { content: string; rule: string; pids: number[]; + problemConfig: Record; rated?: boolean; _code?: string; assign?: string[]; @@ -267,7 +275,9 @@ export interface Tdoc extends Document { lockAt?: Date; unlocked?: boolean; autoHide?: boolean; + /** @deprecated */ balloon?: Record; + /** @deprecated */ score?: Record; langs?: string[]; diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index b86f95b660..85e45e765c 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -160,7 +160,7 @@ const acm = buildContestRule({ } else { columns.push({ type: 'problem', - value: getAlphabeticId(i - 1), + value: tdoc.problemConfig[pid]?.label || getAlphabeticId(i - 1), raw: pid, }); } @@ -296,7 +296,7 @@ const oi = buildContestRule({ } } for (const i in display) { - score += ((tdoc.score?.[i] || 100) * (display[i].score || 0)) / 100; + score += ((tdoc.problemConfig[Number(i)]?.score || 100) * (display[i].score || 0)) / 100; } return { score, detail, display }; }, @@ -317,17 +317,18 @@ const oi = buildContestRule({ columns.push({ type: 'total_score', value: _('Total Score') }); 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, pdict[tdoc.pids[i - 1]].title), + value: '#{0} {1}'.format(i, pdict[pid].title), }); } else { columns.push({ type: 'problem', - value: getAlphabeticId(i - 1), - raw: tdoc.pids[i - 1], + value: cp?.label || getAlphabeticId(i - 1), + raw: pid, }); } } @@ -340,7 +341,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.problemConfig[pid]?.score || 100) / 100); }; if (config.isExport && config.showDisplayName) { row.push({ type: 'email', value: udoc.mail }); @@ -478,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.score?.[i] || 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) { @@ -505,6 +506,7 @@ const strictioi = buildContestRule({ } for (const pid of tdoc.pids) { const index = `${tsdoc.uid}/${tdoc.domainId}/${pid}`; + 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() @@ -513,17 +515,19 @@ const strictioi = buildContestRule({ type: 'records', value: '', raw: [{ - value: ((tsddict[pid]?.score || 0) * ((tdoc.score?.[pid] || 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.score?.[pid] || 100) / 100)).toString() || '', + value: ( + (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.score?.[pid] || 100) / 100)).toString() || '', + value: ((tsddict[pid]?.score || 0) * (fullMark / 100)).toString() || '', raw: tsddict[pid]?.rid, score: tsddict[pid]?.score, }; @@ -564,7 +568,7 @@ const ledo = buildContestRule({ let originalScore = 0; for (const pid of tdoc.pids) { if (!detail[pid]) continue; - const rate = (tdoc.score?.[pid] || 100) / 100; + const rate = (tdoc.problemConfig[pid]?.score || 100) / 100; score += detail[pid].penaltyScore * rate; originalScore += detail[pid].score * rate; } @@ -601,7 +605,7 @@ const ledo = buildContestRule({ for (const pid of tdoc.pids) { row.push({ type: 'record', - value: ((tsddict[pid]?.penaltyScore || 0) * ((tdoc.score?.[pid] || 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, @@ -627,7 +631,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.pids.find((pid) => pid === j.pid)) effective[j.pid] = j; } function time(jdoc) { const real = (jdoc.rid.getTimestamp().getTime() - tdoc.beginAt.getTime()) / 1000; @@ -635,7 +639,7 @@ const homework = buildContestRule({ } function penaltyScore(jdoc) { - const rate = (tdoc.score?.[jdoc.pid] || 100) / 100; + const rate = (tdoc.problemConfig[Number(jdoc.pid)]?.score || 100) / 100; const exceedSeconds = Math.floor( (jdoc.rid.getTimestamp().getTime() - tdoc.penaltySince.getTime()) / 1000, ); @@ -701,7 +705,7 @@ const homework = buildContestRule({ } else { columns.push({ type: 'problem', - value: getAlphabeticId(i - 1), + value: tdoc.problemConfig[pid]?.label || getAlphabeticId(i - 1), raw: pid, }); } @@ -793,11 +797,12 @@ 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'); + data.problemConfig ||= {}; Object.assign(data, { content, owner, title, rule, beginAt, endAt, pids, attend: 0, }); @@ -879,7 +884,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.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'); @@ -937,7 +942,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/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 5b63cc9c70..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'; @@ -617,4 +618,29 @@ 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, { + 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); + }); + return true; + }, ]; diff --git a/packages/onsite-toolkit/index.ts b/packages/onsite-toolkit/index.ts index 228469cfe9..d03b16c80e 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'; @@ -48,7 +48,7 @@ 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 []; @@ -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.pids.map((i, n) => ({ name: pid(n), id: i.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, @@ -192,14 +192,14 @@ export function apply(ctx: Context) { height: 1080, }], })), - ...tdoc.pids.map((i, idx) => getFeed('problems', { - id: `${i}`, - label: String.fromCharCode(65 + idx), - name: pdict[i].title, + ...tdoc.pids.map((pid, idx) => getFeed('problems', { + id: `${pid}`, + label: tdoc.problemConfig[pid]?.label || getAlphabeticId(idx), + name: pdict[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.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, })), ]; diff --git a/packages/scoreboard-xcpcio/index.ts b/packages/scoreboard-xcpcio/index.ts index aa36211209..7ab7c19f07 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.pids.length, - problem_id: tdoc.pids.map((i, idx) => String.fromCharCode(65 + idx)), + 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.balloon - ? tdoc.pids.filter((i) => tdoc.balloon[i]).map((i) => ({ + balloon_color: tdoc.pids.some((pid) => tdoc.problemConfig[pid]?.balloon) + ? tdoc.pids.filter((pid) => tdoc.problemConfig[pid]?.balloon).map((pid) => ({ color: '#000', - background_color: typeof tdoc.balloon[i] === 'string' ? tdoc.balloon[i] : tdoc.balloon[i].color, + background_color: tdoc.problemConfig[pid].balloon.color, })) : [], logo: { 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/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 new file mode 100644 index 0000000000..24d128ad5a --- /dev/null +++ b/packages/ui-default/components/contestProblemEditor/ContestProblemEditor.tsx @@ -0,0 +1,225 @@ +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'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { api, i18n } from 'vj/utils'; +import ProblemSelectAutoComplete from '../autocomplete/components/ProblemSelectAutoComplete'; + +interface Problem extends ContestProblemConfig { + pid: number; + _tmpId?: string; // use this as key +} + +export interface ContestProblemEditorProps { + pids: number[]; + problemConfig: Record; + onChange: (pids: number[], problemConfig: Record) => void; +} +const randomId = () => Math.random().toString(16).substring(2); +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 ( + + + + + + { problemRefs.current[index] = ref; }} + onChange={(v) => handleChange(index, 'pid', v)} + selectedKeys={[problem.pid.toString()]} + /> + + {problemRawTitles[problem.pid]} + + handleChange(index, 'label', e.target.value)} + /> + + + handleChange(index, 'score', parseInt(e.target.value, 10) || 0)} + min={0} + /> + + + handleRemove(index)}> + {i18n('Remove')} + + + + ); +}; + +const ContestProblemEditor = ({ pids: initialPids, problemConfig: initialProblemConfig, onChange: _onChange }: ContestProblemEditorProps) => { + // TODO: also support balloon and other fields in the future + 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>({}); + + const fetchProblemTitles = debounce(async (ids: number[]) => { + api('problems', { ids }, ['docId', 'pid', 'title']) + .then((res) => { + setProblemRawTitles(res.reduce((acc, cur) => { + acc[cur.docId] = cur.title; + return acc; + }, {})); + }) + .catch(() => { + // pid maybe not exist + }); + }, 500); + + React.useEffect(() => { + fetchProblemTitles(problems.map((i) => i.pid).filter((i) => i)); + }, []); + + const onChange = (newProblems: Problem[]) => { + const fixedProblems = newProblems.map((i) => { + const problem = { ...i }; + if (problem.score === 100) problem.score = undefined; + return problem; + }); + setProblems(fixedProblems); + + 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(cp).length > 0) acc[cur.pid] = cp; + return acc; + }, {}); + _onChange(pids, problemConfig); + }; + + const handleAdd = () => { + const newProblems = [...problems, { pid: 0, label: getAlphabeticId(problems.length), _tmpId: randomId() }]; + setProblems(newProblems); + onChange(newProblems); + }; + + const handleRemove = (index: number) => { + const newProblems = problems.filter((_, i) => i !== index); + setProblems(newProblems); + onChange(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 as any)[field] = value; + } + + newProblems[index] = problem; + setProblems(newProblems); + if (field === 'pid') { + fetchProblemTitles(newProblems.map((i) => i.pid).filter((i) => i)); + } + onChange(newProblems); + }; + + const moveRow = (dragIndex: number, hoverIndex: number) => { + const newProblems = [...problems]; + + const [movedItem] = newProblems.splice(dragIndex, 1); + newProblems.splice(hoverIndex, 0, movedItem); + + setProblems(newProblems); + onChange(newProblems); + }; + + return ( + +
+ + + + + + + + + + + + + {problems.map((problem, index) => ( + + ))} + +
pid{i18n('Title')}{i18n('Label')}{i18n('Score')}{i18n('Action')}
+
+ +
+
+
+ ); +}; + +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/pages/contest_balloon.page.tsx b/packages/ui-default/pages/contest_balloon.page.tsx index 29c4aca2a7..975bed8a71 100644 --- a/packages/ui-default/pages/contest_balloon.page.tsx +++ b/packages/ui-default/pages/contest_balloon.page.tsx @@ -26,14 +26,15 @@ function Balloon({ tdoc, val }) { - {tdoc.pids.map((pid) => { + {tdoc.pids.map((pid, idx) => { + const cp = tdoc.problemConfig[pid]; const { color: c, name } = val[+pid]; return ( {now === pid - ? ({getAlphabeticId(tdoc.pids.indexOf(+pid))}) - : ({getAlphabeticId(tdoc.pids.indexOf(+pid))})} + ? ({cp?.label || getAlphabeticId(idx)}) + : ({cp?.label || getAlphabeticId(idx)})} { 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 +67,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 pid of tdoc.pids) { + val[+pid] = tdoc.problemConfig[pid]?.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.tsx similarity index 78% rename from packages/ui-default/pages/contest_edit.page.ts rename to packages/ui-default/pages/contest_edit.page.tsx index a0cbad85da..4c8e232a85 100644 --- a/packages/ui-default/pages/contest_edit.page.ts +++ b/packages/ui-default/pages/contest_edit.page.tsx @@ -1,16 +1,30 @@ import $ from 'jquery'; import moment from 'moment'; +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 { confirm } from 'vj/components/dialog'; import { NamedPage } from 'vj/misc/Page'; import { i18n, request } from 'vj/utils'; 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 }); + if ($('#problem-editor').length) { + const pidsInput = $('[name=pids]'); + const problemConfigInput = $('[name=problemConfig]'); + ReactDOM.createRoot($('#problem-editor')[0]).render( + +i)} + problemConfig={JSON.parse(problemConfigInput.val() as string)} + onChange={(pids, problemConfig) => { + pidsInput.val(pids.join(',')); + problemConfigInput.val(JSON.stringify(problemConfig)); + }} + />, + ); + } $('[name="rule"]').on('change', () => { const rule = $('[name="rule"]').val(); $('.contest-rule-settings input').attr('disabled', 'disabled'); @@ -34,6 +48,7 @@ const page = new NamedPage(['contest_edit', 'contest_create', 'homework_create', }).trigger('change'); if (pagename.endsWith('edit')) { let confirmed = false; + // eslint-disable-next-line consistent-return $(document).on('click', '[value="delete"]', (ev) => { ev.preventDefault(); if (confirmed) { diff --git a/packages/ui-default/templates/components/contest.html b/packages/ui-default/templates/components/contest.html index 923bbab4a3..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 %}{{ utils.getAlphabeticId(tdoc.pids.indexOf(subject)) }}. {{ 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/components/problem.html b/packages/ui-default/templates/components/problem.html index 5e63aafb38..857bf59d03 100644 --- a/packages/ui-default/templates/components/problem.html +++ b/packages/ui-default/templates/components/problem.html @@ -1,15 +1,15 @@ -{% 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 %} -{%- 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..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 not tdoc.balloon|length %} + {% 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 903213ec03..23eeff81cf 100644 --- a/packages/ui-default/templates/contest_edit.html +++ b/packages/ui-default/templates/contest_edit.html @@ -63,14 +63,37 @@

{{ _('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|default(utils.getAlphabeticId(tdoc.pid2idx[p.pid])) }}{{ 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..5dada0495c 100644 --- a/packages/ui-default/templates/contest_manage.html +++ b/packages/ui-default/templates/contest_manage.html @@ -22,6 +22,7 @@ {%- for pid in tdoc.pids -%} + {% set cp = utils.getContestProblemConfig(pid, tdoc) %} @@ -29,7 +30,7 @@ - {{ tdoc.score[pid]|default(100) }} + {{ cp.score|default(100) }} {%- endfor -%} @@ -127,7 +128,7 @@

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

{% for pid in tdoc.pids %} - + {% endfor %} diff --git a/packages/ui-default/templates/contest_problemlist.html b/packages/ui-default/templates/contest_problemlist.html index 8f5b8e90f7..b9cf9f0c7e 100644 --- a/packages/ui-default/templates/contest_problemlist.html +++ b/packages/ui-default/templates/contest_problemlist.html @@ -42,12 +42,13 @@

{{ _('Problems') }}

{%- 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 %} {% 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) }} @@ -185,7 +186,8 @@

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

{% for pid in tdoc.pids %} - + {% set cp = utils.getContestProblemConfig(pid, tdoc) %} + {% endfor %} diff --git a/packages/ui-default/templates/homework_detail.html b/packages/ui-default/templates/homework_detail.html index b7bae2123b..a9009bb34f 100644 --- a/packages/ui-default/templates/homework_detail.html +++ b/packages/ui-default/templates/homework_detail.html @@ -58,9 +58,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], 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) }} + {{ 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..a8a1acb2ed 100644 --- a/packages/ui-default/templates/homework_edit.html +++ b/packages/ui-default/templates/homework_edit.html @@ -84,12 +84,37 @@ - {{ 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|default(utils.getAlphabeticId(tdoc.pid2idx[p.pid])) }}{{ 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..36dd1d69fd 100644 --- a/packages/ui-default/templates/partials/contest_balloon.html +++ b/packages/ui-default/templates/partials/contest_balloon.html @@ -15,7 +15,8 @@ {% endif %} {{ bdoc._id.toHexString()|truncate(8,true,'') }} - + {% set cp = utils.getContestProblemConfig(bdoc.pid, tdoc) %} + {% if not bdoc.sent %}
@@ -25,7 +26,7 @@
{% endif %} - {{ utils.getAlphabeticId(tdoc.pids.indexOf(bdoc.pid)) }}  ({{ tdoc.balloon[bdoc.pid].name }}) + {{ cp.label }}  ({{ cp.balloon.name }}) {{ user.render_inline(udict[bdoc.uid], badge=false) }} diff --git a/packages/ui-default/templates/partials/scoreboard.html b/packages/ui-default/templates/partials/scoreboard.html index 7f3343f54a..c8ae4e1d3f 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 %} @@ -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 -%} @@ -67,7 +67,7 @@ {%- endif -%} {%- endfor -%} {%- else -%} - {{ column.value|string|nl2br|safe }} + {{ column.value|string|nl2br|safe }} {%- endif -%} {%- endfor -%} diff --git a/packages/ui-default/templates/problem_detail.html b/packages/ui-default/templates/problem_detail.html index 81429dd4ab..11aa9d537f 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 %}
{% 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 %} - {{ 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..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 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..5f9aa6c04f 100644 --- a/packages/ui-default/templates/record_main_tr.html +++ b/packages/ui-default/templates/record_main_tr.html @@ -17,8 +17,10 @@ | {% endif %} - {% if pdoc and rdoc.contest %} - {{ problem.render_problem_title(pdoc, tdoc=tdoc, show_tags=false, show_invisible_flag=false) }} + {% 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, 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 %} diff --git a/packages/utils/lib/common.ts b/packages/utils/lib/common.ts index 99f1bd23c8..5a746f9049 100644 --- a/packages/utils/lib/common.ts +++ b/packages/utils/lib/common.ts @@ -245,3 +245,13 @@ 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) => { + 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 || getAlphabeticId(idx), + }; +};