diff --git a/package.json b/package.json index 95518e417..81e7805bf 100644 --- a/package.json +++ b/package.json @@ -28,27 +28,27 @@ "license": "AGPL-3.0-only", "devDependencies": { "@types/cross-spawn": "^6.0.2", - "@types/node": "^16.11.19", + "@types/node": "^16.11.21", "@types/semver": "^7.3.9", - "@typescript-eslint/eslint-plugin": "5.9.0", - "@typescript-eslint/parser": "5.9.0", + "@typescript-eslint/eslint-plugin": "5.10.0", + "@typescript-eslint/parser": "5.10.0", "cac": "^6.7.12", "cross-env": "^7.0.3", "cross-spawn": "^7.0.3", "esbuild": "0.14.3", - "eslint": "^8.6.0", + "eslint": "^8.7.0", "eslint-config-airbnb-typescript": "12.3.1", "eslint-import-resolver-typescript": "2.5.0", "eslint-plugin-import": "2.25.4", "eslint-plugin-simple-import-sort": "7.0.0", "fs-extra": "^10.0.0", - "globby": "11.0.4", + "globby": "11.1.0", "latest-version": "^6.0.0", "mongodb": "^3.7.3", "nmls": "^3.0.1", "ora": "5.4.1", "semver": "^7.3.5", - "typedoc": "^0.22.10", + "typedoc": "^0.22.11", "typescript": "4.4.3" } } diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index dba32bb0f..5b9fe9434 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "3.3.20", + "version": "3.4.0", "bin": "bin/hydrooj.js", "main": "src/loader", "module": "src/loader", @@ -22,7 +22,7 @@ "detect-browser": "^5.3.0", "esbuild": "0.14.3", "fs-extra": "^10.0.0", - "graphql": "^16.2.0", + "graphql": "^16.3.0", "graphql-scalars": "1.14.1", "js-yaml": "^4.1.0", "koa": "^2.13.4", @@ -40,7 +40,7 @@ "nanoid": "^3.2.0", "nodemailer": "^6.7.2", "notp": "^2.0.3", - "p-queue": "^7.1.0", + "p-queue": "^7.2.0", "reflect-metadata": "^0.1.13", "require-resolve-hook": "^1.1.0", "semver": "^7.3.5", @@ -62,7 +62,7 @@ "@types/lodash": "^4.14.178", "@types/lru-cache": "^5.1.1", "@types/mime-types": "^2.1.1", - "@types/minio": "^7.0.11", + "@types/minio": "^7.0.12", "@types/mongodb": "^3.6.20", "@types/nodemailer": "^6.4.4", "@types/notp": "^2.0.1", @@ -70,7 +70,7 @@ "@types/serialize-javascript": "^5.0.2", "@types/sockjs": "^0.3.33", "@types/source-map-support": "^0.5.4", - "@types/superagent": "^4.1.14", + "@types/superagent": "^4.1.15", "moment": "^2.29.1", "pm2": "^5.1.2" }, diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index f5c0ffa77..bfd88fe08 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -47,13 +47,18 @@ registerResolver( 'Get a contest by ID', ); -TaskModel.Worker.addHandler('contest.problemHide', async (doc) => { +TaskModel.Worker.addHandler('contest', async (doc) => { const tdoc = await contest.get(doc.domainId, doc.tid); if (!tdoc) return; - for (const pid of tdoc.pids) { - // eslint-disable-next-line no-await-in-loop - await problem.edit(doc.domainId, pid, { hidden: false }); + const tasks = []; + for (const op of doc.operation) { + if (op === 'unhide') { + for (const pid of tdoc.pids) { + tasks.push(problem.edit(doc.domainId, pid, { hidden: false })); + } + } else if (op === 'unlock') tasks.push(contest.recalcStatus(doc.domainId, doc.tid)); } + await Promise.all(tasks); }); export class ContestListHandler extends Handler { @@ -129,7 +134,7 @@ export class ContestDetailHandler extends Handler { if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); await contest.del(domainId, tid); await TaskModel.deleteMany({ - type: 'schedule', subType: 'contest.problemHide', domainId, tid, + type: 'schedule', subType: 'contest', domainId, tid, }); this.response.redirect = this.url('contest_main'); } @@ -167,8 +172,13 @@ export class ContestBroadcastHandler extends Handler { export class ContestScoreboardHandler extends Handler { @param('tid', Types.ObjectID) @param('page', Types.PositiveInt, true) - async get(domainId: string, tid: ObjectID, page = 1) { - const [tdoc, rows, udict, , nPages] = await contest.getScoreboard.call(this, domainId, tid, false, page); + @param('ignoreLock', Types.Boolean, true) + async get(domainId: string, tid: ObjectID, page = 1, ignoreLock = false) { + const tdoc = await contest.get(domainId, tid); + if (ignoreLock && !this.user.own(tdoc)) { + this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); + } + const [, rows, udict, , nPages] = await contest.getScoreboard.call(this, domainId, tid, false, page, ignoreLock); const path = [ ['Hydro', 'homepage'], ['contest_main', 'contest_main'], @@ -185,13 +195,18 @@ export class ContestScoreboardHandler extends Handler { export class ContestScoreboardDownloadHandler extends Handler { @param('tid', Types.ObjectID) @param('ext', Types.Range(['csv', 'html'])) - async get(domainId: string, tid: ObjectID, ext: string) { + @param('ignoreLock', Types.Boolean, true) + async get(domainId: string, tid: ObjectID, ext: string, ignoreLock = false) { await this.limitRate('scoreboard_download', 120, 3); const getContent = { csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`, html: (rows, tdoc) => this.renderHTML('contest_scoreboard_download_html.html', { rows, tdoc }), }; - const [tdoc, rows] = await contest.getScoreboard.call(this, domainId, tid, true, 0); + const tdoc = await contest.get(domainId, tid); + if (ignoreLock && !this.user.own(tdoc)) { + this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); + } + const [, rows] = await contest.getScoreboard.call(this, domainId, tid, true, 0, ignoreLock); this.binary(await getContent[ext](rows, tdoc), `${tdoc.title}.${ext}`); } } @@ -240,10 +255,11 @@ export class ContestEditHandler extends Handler { @param('code', Types.String, true) @param('autoHide', Types.String, true) @param('assign', Types.CommaSeperatedArray, true) + @param('lock', Types.UnsignedInt, true) async post( domainId: string, tid: ObjectID, beginAtDate: string, beginAtTime: string, duration: number, title: string, content: string, rule: string, _pids: string, rated = false, - _code = '', autoHide = false, assign: string[] = null, + _code = '', autoHide = false, assign: string[] = null, lock: number = null, ) { if (autoHide) this.checkPerm(PERM.PERM_EDIT_PROBLEM); const pids = _pids.replace(/,/g, ',').split(',').map((i) => +i).filter((i) => i); @@ -252,20 +268,22 @@ export class ContestEditHandler extends Handler { const endAt = beginAtMoment.clone().add(duration, 'hours').toDate(); if (beginAtMoment.isSameOrAfter(endAt)) throw new ValidationError('duration'); const beginAt = beginAtMoment.toDate(); + const lockAt = lock ? moment(endAt).add(-lock, 'minutes').toDate() : null; await problem.getList(domainId, pids, this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id, this.user.group, true); if (tid) { await contest.edit(domainId, tid, { title, content, rule, beginAt, endAt, pids, rated, }); if (this.tdoc.beginAt !== beginAt || this.tdoc.endAt !== endAt - || Array.isDiff(this.tdoc.pids, pids) || this.tdoc.rule !== rule) { + || Array.isDiff(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); } const task = { - type: 'schedule', subType: 'contest.problemHide', domainId, tid, + type: 'schedule', subType: 'contest', domainId, tid, }; await TaskModel.deleteMany(task); if (Date.now() <= endAt.getTime() && autoHide) { @@ -273,10 +291,20 @@ export class ContestEditHandler extends Handler { await Promise.all(pids.map((pid) => problem.edit(domainId, pid, { hidden: true }))); await TaskModel.add({ ...task, + operation: ['unhide'], executeAfter: endAt, }); } - await contest.edit(domainId, tid, { assign, _code, autoHide }); + if (lock && lockAt <= endAt) { + await TaskModel.add({ + ...task, + operation: ['unlock'], + executeAfter: endAt, + }); + } + await contest.edit(domainId, tid, { + assign, _code, autoHide, lockAt, + }); this.response.body = { tid }; this.response.redirect = this.url('contest_detail', { tid }); } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index cc46bf6e4..12ba025db 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -348,6 +348,9 @@ export interface Tdoc { detail: any, } -export interface ContestRule { +export interface ContestRule { + _originalRule?: Partial>; TEXT: string; check: (args: any) => any; statusSort: any; + submitAfterAccept: boolean; showScoreboard: (tdoc: Tdoc<30>, now: Date) => boolean; showSelfRecord: (tdoc: Tdoc<30>, now: Date) => boolean; showRecord: (tdoc: Tdoc<30>, now: Date) => boolean; - stat: (tdoc: Tdoc<30>, journal: any[]) => ContestStat; + stat: (this: ContestRule, tdoc: Tdoc<30>, journal: any[], ignoreLock?: boolean) => ContestStat & T; scoreboard: ( - isExport: boolean, _: (s: string) => string, - tdoc: Tdoc<30>, pdict: ProblemDict, cursor: Cursor, page: number, + this: ContestRule, isExport: boolean, _: (s: string) => string, + tdoc: Tdoc<30>, pdict: ProblemDict, cursor: Cursor, page: number, ) => Promise<[board: ScoreboardRow[], udict: Udict, nPages: number]>; - ranked: (tdoc: Tdoc<30>, cursor: Cursor) => Promise; + ranked: (tdoc: Tdoc<30>, cursor: Cursor) => Promise<[Array<[number, ContestStat & T]>, number]>; } export type ContestRules = Dictionary; diff --git a/packages/hydrooj/src/model/contest.ts b/packages/hydrooj/src/model/contest.ts index e245f6743..3f449ac28 100644 --- a/packages/hydrooj/src/model/contest.ts +++ b/packages/hydrooj/src/model/contest.ts @@ -1,4 +1,5 @@ import { FilterQuery, ObjectID } from 'mongodb'; +import { Counter } from '@hydrooj/utils/lib/utils'; import { ContestAlreadyAttendedError, ContestNotAttendedError, ContestNotFoundError, ContestScoreboardHiddenError, ValidationError, @@ -11,6 +12,7 @@ import { import * as misc from '../lib/misc'; import ranked from '../lib/rank'; import * as bus from '../service/bus'; +import type { Handler } from '../service/server'; import { PERM, STATUS } from './builtin'; import * as document from './document'; import problem from './problem'; @@ -29,41 +31,51 @@ interface AcmDetail extends AcmJournal { real: number; } -const acm: ContestRule = { +function buildContestRule(def: ContestRule): ContestRule { + def._originalRule = { scoreboard: def.scoreboard, stat: def.stat }; + def.scoreboard = (def._originalRule?.scoreboard || def.scoreboard).bind(def); + def.stat = (def._originalRule?.stat || def.stat).bind(def); + return def; +} + +function filterEffective(tdoc: Tdoc, journal: T[], ignoreLock = false): T[] { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + if (isLocked(tdoc) && !ignoreLock) journal = journal.filter((i) => i.rid.generationTime * 1000 < tdoc.lockAt.getTime()); + return journal.filter((i) => tdoc.pids.includes(i.pid)); +} + +const acm = buildContestRule({ TEXT: 'ACM/ICPC', check: () => { }, statusSort: { accept: -1, time: 1 }, + submitAfterAccept: false, showScoreboard: () => true, showSelfRecord: () => true, showRecord: (tdoc, now) => now > tdoc.endAt, - stat: (tdoc, journal: AcmJournal[]) => { - const naccept: Record = {}; + stat(tdoc, journal: AcmJournal[], ignoreLock = false) { + const naccept = Counter(); const effective: Record = {}; const detail: AcmDetail[] = []; let accept = 0; let time = 0; - for (const j of journal) { - if (tdoc.pids.includes(j.pid) - && effective[j.pid]?.status !== STATUS.STATUS_ACCEPTED) { - effective[j.pid] = j; - if (![STATUS.STATUS_ACCEPTED, STATUS.STATUS_COMPILE_ERROR].includes(j.status)) { - naccept[j.pid] = (naccept[j.pid] || 0) + 1; - } + for (const j of filterEffective(tdoc, journal, ignoreLock)) { + if (!this.submitAfterAccept && effective[j.pid]?.status === STATUS.STATUS_ACCEPTED) continue; + effective[j.pid] = j; + if (![STATUS.STATUS_ACCEPTED, STATUS.STATUS_COMPILE_ERROR].includes(j.status)) { + naccept[j.pid]++; } } - for (const key in effective) { - const j = effective[key]; + for (const pid in effective) { + const j = effective[pid]; const real = j.rid.generationTime - Math.floor(tdoc.beginAt.getTime() / 1000); - const penalty = 20 * 60 * (naccept[j.pid] || 0); + const penalty = 20 * 60 * naccept[j.pid]; detail.push({ - ...j, naccept: naccept[j.pid] || 0, time: real + penalty, real, penalty, + ...j, naccept: naccept[j.pid], time: real + penalty, real, penalty, }); } - for (const d of detail) { - if (d.status === STATUS.STATUS_ACCEPTED) { - accept += 1; - time += d.time; - } + for (const d of detail.filter((i) => i.status === STATUS.STATUS_ACCEPTED)) { + accept++; + time += d.time; } return { accept, time, detail }; }, @@ -80,7 +92,6 @@ const acm: ContestRule = { columns.push( { type: 'total_time', value: _('Penalty') }, { type: 'total_time', value: _('Total Time (Seconds)') }, - { type: 'total_time_str', value: _('Total Time') }, ); } for (let i = 1; i <= tdoc.pids.length; i++) { @@ -108,41 +119,43 @@ const acm: ContestRule = { }); } } + + // Find first accept const first = {}; for (const pid of tdoc.pids) first[pid] = new ObjectID().generationTime; for (const [, tsdoc] of rankedTsdocs) { const tsddict = {}; - for (const item of tsdoc.journal || []) tsddict[item.pid] = item; + for (const item of tsdoc.detail || []) tsddict[item.pid] = item; for (const pid of tdoc.pids) { if (tsddict[pid]?.status === STATUS.STATUS_ACCEPTED && tsddict[pid].rid.generationTime < first[pid]) { first[pid] = tsddict[pid].rid.generationTime; } } } + const rows: ScoreboardRow[] = [columns]; for (const [rank, tsdoc] of rankedTsdocs) { - const tsddict = {}; - for (const item of tsdoc.detail || []) tsddict[item.pid] = item; + const tsddict: Record = {}; + for (const item of tsdoc.detail) tsddict[item.pid] = item; const row: ScoreboardRow = [ { type: 'string', value: rank.toString() }, { type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid }, { type: 'string', value: tsdoc.accept || 0 }, ]; if (isExport) { - const penalty = Math.sum(tdoc.pids.map((i) => (tsddict[i]?.naccept || 0))) * 20 * 60; + const penalty = Math.sum(tdoc.pids.map((i) => tsddict[i]?.naccept || 0)) * 20 * 60; row.push( { type: 'string', value: penalty.toString() }, { type: 'string', value: tsdoc.time || 0.0 }, - { type: 'string', value: tsdoc.time || 0.0 }, ); } for (const pid of tdoc.pids) { - const doc = tsddict[pid] || {}; + const doc = tsddict[pid] || {} as Partial; const accept = doc.status === STATUS.STATUS_ACCEPTED; const rid = accept ? doc.rid : null; const colAccepted = `${(accept && isExport) ? `${_('Accepted')} ` : ''}${doc.naccept ? ` (-${doc.naccept})` : ''}`; - const colTime = accept ? doc.time : '-'; - const colTimeStr = accept ? misc.formatSeconds(colTime) : '-'; + const colTime = accept ? doc.time.toString() : '-'; + const colTimeStr = accept ? misc.formatSeconds(doc.time) : '-'; if (isExport) { row.push( { type: 'string', value: colAccepted }, @@ -161,7 +174,6 @@ const acm: ContestRule = { }); } } - row.raw = tsdoc; rows.push(row); } return [rows, udict, nPages]; @@ -169,17 +181,18 @@ const acm: ContestRule = { async ranked(tdoc, cursor) { return await ranked.all(cursor, (a, b) => a.score === b.score); }, -}; +}); -const oi: ContestRule = { +const oi = buildContestRule({ TEXT: 'OI', check: () => { }, + submitAfterAccept: true, statusSort: { score: -1 }, - stat: (tdoc, journal) => { + stat(tdoc, journal) { const detail = {}; let score = 0; - for (const j of journal) { - if (tdoc.pids.includes(j.pid)) detail[j.pid] = j; + for (const j of journal.filter((i) => tdoc.pids.includes(i.pid))) { + if (detail[j.pid]?.status === STATUS.STATUS_ACCEPTED && !this.submitAfterAccept) detail[j.pid] = j; } for (const i in detail) score += detail[i].score; return { score, detail }; @@ -278,19 +291,21 @@ const oi: ContestRule = { async ranked(tdoc, cursor) { return await ranked.all(cursor, (a, b) => a.score === b.score); }, -}; +}); -const ioi: ContestRule = { +const ioi = buildContestRule({ ...oi, TEXT: 'IOI', + submitAfterAccept: false, showRecord: (tdoc, now) => now > tdoc.endAt, showSelfRecord: () => true, showScoreboard: () => true, -}; +}); -const homework: ContestRule = { +const homework = buildContestRule({ TEXT: 'Assignment', check: () => { }, + submitAfterAccept: false, statusSort: { penaltyScore: -1, time: 1 }, stat: (tdoc, journal) => { const effective = {}; @@ -434,7 +449,7 @@ const homework: ContestRule = { async ranked(tdoc, cursor) { return await ranked.all(cursor, (a, b) => a.score === b.score); }, -}; +}); export const RULES: ContestRules = { acm, oi, homework, ioi, @@ -449,12 +464,12 @@ export async function add( rule: string, beginAt = new Date(), endAt = new Date(), pids: number[] = [], rated = false, data: Partial = {}, ) { - if (!this.RULES[rule]) throw new ValidationError('rule'); + if (!RULES[rule]) throw new ValidationError('rule'); if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt'); Object.assign(data, { content, owner, title, rule, beginAt, endAt, pids, attend: 0, }); - this.RULES[rule].check(data); + RULES[rule].check(data); await bus.serial('contest/before-add', data); const res = await document.add(domainId, content, owner, document.TYPE_CONTEST, null, null, null, { ...data, title, rule, beginAt, endAt, pids, attend: 0, rated, @@ -464,10 +479,10 @@ export async function add( } export async function edit(domainId: string, tid: ObjectID, $set: any) { - if ($set.rule && !this.RULES[$set.rule]) throw new ValidationError('rule'); + if ($set.rule && !RULES[$set.rule]) throw new ValidationError('rule'); const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); if (!tdoc) throw new ContestNotFoundError(domainId, tid); - this.RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set)); + RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set)); return await document.set(domainId, document.TYPE_CONTEST, tid, $set); } @@ -489,8 +504,14 @@ export async function getRelated(domainId: string, pid: number) { return await document.getMulti(domainId, document.TYPE_CONTEST, { pids: pid }).toArray(); } -export function getStatus(domainId: string, tid: ObjectID, uid: number) { - return document.getStatus(domainId, document.TYPE_CONTEST, tid, uid); +export async function getStatus(domainId: string, tid: ObjectID, uid: number) { + const [tdoc, status] = await Promise.all([ + get(domainId, tid), + document.getStatus(domainId, document.TYPE_CONTEST, tid, uid), + ]); + // eslint-disable-next-line @typescript-eslint/no-use-before-define + if (isLocked(tdoc)) Object.assign(status, RULES[tdoc.rule].stat(tdoc, status.journal || [], true)); + return status; } export async function updateStatus( @@ -556,6 +577,12 @@ export function isDone(tdoc: Tdoc) { return tdoc.endAt <= new Date(); } +export function isLocked(tdoc: Tdoc) { + if (!tdoc.lockAt) return false; + const now = new Date(); + return (tdoc.lockAt < now && now < tdoc.endAt); +} + export function isExtended(tdoc: Tdoc) { const now = new Date().getTime(); return tdoc.penaltySince.getTime() <= now && now < tdoc.endAt.getTime(); @@ -627,11 +654,12 @@ export function canShowScoreboard(tdoc: Tdoc<30>, allowPermOverride = true) { } export async function getScoreboard( - domainId: string, tid: ObjectID, - isExport = false, page: number, + this: Handler, domainId: string, tid: ObjectID, + isExport = false, page: number, ignoreLock = false, ): Promise<[Tdoc<30 | 60>, ScoreboardRow[], Udict, ProblemDict, number]> { const tdoc = await get(domainId, tid); if (!canShowScoreboard.call(this, tdoc)) throw new ContestScoreboardHiddenError(tid); + if (ignoreLock) delete tdoc.lockAt; const tsdocsCursor = getMultiStatus(domainId, { docId: tid }).sort(RULES[tdoc.rule].statusSort); const pdict = await problem.getList(domainId, tdoc.pids, true); const [rows, udict, nPages] = await RULES[tdoc.rule].scoreboard( @@ -684,6 +712,7 @@ global.Hydro.model.contest = { isNotStarted, isOngoing, isDone, + isLocked, isExtended, statusText, getStatusText, diff --git a/packages/hydrooj/src/model/problem.ts b/packages/hydrooj/src/model/problem.ts index 986dbde30..40df915fc 100644 --- a/packages/hydrooj/src/model/problem.ts +++ b/packages/hydrooj/src/model/problem.ts @@ -286,6 +286,7 @@ export class ProblemModel { group: string[] = [], doThrow = true, projection = ProblemModel.PROJECTION_PUBLIC, indexByDocIdOnly = false, ): Promise { + if (!pids?.length) return []; const r: Record = {}; const l: Record = {}; const q: any = { docId: { $in: pids } }; diff --git a/packages/hydrooj/src/model/task.ts b/packages/hydrooj/src/model/task.ts index 19048eb2b..a660a62b4 100644 --- a/packages/hydrooj/src/model/task.ts +++ b/packages/hydrooj/src/model/task.ts @@ -70,7 +70,10 @@ class WorkerService implements BaseService { try { logger.debug('Worker task: %o', doc); const start = Date.now(); - await this.handlers[doc.subType](doc); + await Promise.race([ + this.handlers[doc.subType](doc), + new Promise((resolve) => setTimeout(resolve, 300000)), + ]); const spent = Date.now() - start; if (spent > 500) logger.warn('Slow worker task (%d ms): %s', spent, doc); } catch (e) { diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 1d7b97503..b208116f7 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -21,6 +21,7 @@ import problem from './model/problem'; import RecordModel from './model/record'; import StorageModel from './model/storage'; import * as system from './model/system'; +import TaskModel from './model/task'; import user from './model/user'; import { iterateAllDomain, iterateAllProblem, iterateAllPsdoc, iterateAllUser, @@ -673,6 +674,14 @@ const scripts: UpgradeScript[] = [ ); return true; }, + async function _58_59() { + const _FRESH_INSTALL_IGNORE = 1; + const tasks = await db.collection('task').find({ type: 'schedule', subType: 'contest.problemHide' }).toArray(); + for (const task of tasks) { + await TaskModel.add({ ...task, subType: 'contest', operation: ['unhide'] }); + } + return true; + }, ]; export default scripts; diff --git a/packages/ui-default/components/monaco/index.ts b/packages/ui-default/components/monaco/index.ts index 80d9189d8..17fddd332 100644 --- a/packages/ui-default/components/monaco/index.ts +++ b/packages/ui-default/components/monaco/index.ts @@ -27,7 +27,7 @@ class ChangeThemeAction extends EditorAction { super({ id: 'hydro.changeEditorTheme', label: i18n('Change Theme'), - alias: i18n('Change Theme'), + alias: [i18n('Change Theme'), 'Change Theme'], }); } diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index 26db799cb..73c5372dc 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -663,6 +663,7 @@ Saving file...: 正在保存文件... score canceled: 成绩取消 Score: 分数 Scoreboard (Hidden): 成绩表 (隐藏) +Scoreboard locked at {0}: 比赛已于 {0} 封榜。 Scoreboard: 成绩表 Scripts: 脚本 Search: 搜索 diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 9d475f30d..5445bd4f8 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.30.9", + "version": "4.30.10", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", @@ -25,15 +25,15 @@ "chalk": "^4.1.2", "classnames": "^2.3.1", "clean-webpack-plugin": "^4.0.0", - "clipboard": "^2.0.8", + "clipboard": "^2.0.9", "copy-webpack-plugin": "^6.4.1", "css-loader": "^4.3.0", "diff-dom": "^4.2.3", - "echarts": "^5.2.2", + "echarts": "^5.3.0", "emojis-keywords": "2.0.0", "emojis-list": "2.1.0", "esbuild-loader": "^2.18.0", - "eslint": "^8.7.0", + "eslint": "^8.8.0", "eslint-config-airbnb": "^18.2.1", "eslint-import-resolver-webpack": "^0.13.2", "eslint-plugin-import": "^2.25.4", @@ -92,7 +92,7 @@ "through2": "^4.0.2", "timeago-react": "^3.0.4", "timeago.js": "^4.0.2", - "vditor": "^3.8.10", + "vditor": "^3.8.11", "vinyl-buffer": "^1.0.1", "web-streams-polyfill": "^3.2.0", "webpack": "^4.46.0", @@ -116,7 +116,7 @@ "markdown-it-table-of-contents": "^0.6.0", "mongodb": "^3.7.3", "nunjucks": "^3.2.3", - "react-query": "^3.34.8", + "react-query": "^3.34.12", "streamsaver": "^2.0.5", "xss": "^1.0.10" } diff --git a/packages/ui-default/pages/home_preference.page.jsx b/packages/ui-default/pages/home_preference.page.jsx index 38d4d22c0..e9a85467c 100644 --- a/packages/ui-default/pages/home_preference.page.jsx +++ b/packages/ui-default/pages/home_preference.page.jsx @@ -1,65 +1,16 @@ import { NamedPage } from 'vj/misc/Page'; -import code from 'vj/components/highlighter/code-example'; import i18n from 'vj/utils/i18n'; import tpl from 'vj/utils/tpl'; -const page = new NamedPage('home_preference', async () => { - async function mountComponent() { - const [{ default: prismjs }, React, { render }] = await Promise.all([ - import('vj/components/highlighter/prismjs'), - import('react'), - import('react-dom'), - ]); +const page = new NamedPage('home_preference', () => { + function setOptions($el, options) { + $el.empty(); + $.each(options, (key, value) => { + $el.append($('').attr('value', key).text(value)); + }); + } - class AstylePreview extends React.PureComponent { - constructor(props) { - super(props); - this.state = { highlight: prismjs.highlight(this.preview, prismjs.Prism.languages.cpp, 'C++') }; - } - - render() { - return ( - <> -
-