diff --git a/packages/hydrooj/locales/en.yaml b/packages/hydrooj/locales/en.yaml index 62caaac29..118c63b7b 100644 --- a/packages/hydrooj/locales/en.yaml +++ b/packages/hydrooj/locales/en.yaml @@ -45,6 +45,7 @@ homework_main: Homework homework_scoreboard: Scoreboard judge_playground: Judge Playground main: Home +manage_rejudge: Rejudge Management manage_config: System Configurations manage_dashboard: System manage_module: Module Management diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index 988e63af5..a4f0bf90a 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -465,6 +465,7 @@ Logout: 登出 Lost Password: 忘记密码 Lucky: 手气不错 Mail From: 发件人 +manage_rejudge: 重测管理 manage_dashboard: 控制面板 manage_join_applications: 加域申请 manage_module: 管理模块 diff --git a/packages/hydrooj/locales/zh_TW.yaml b/packages/hydrooj/locales/zh_TW.yaml index e5b87ed13..f4600da07 100644 --- a/packages/hydrooj/locales/zh_TW.yaml +++ b/packages/hydrooj/locales/zh_TW.yaml @@ -252,6 +252,7 @@ Logout All Sessions: 登出所有會話 Logout This Session: 登出該會話 Logout: 登出 Lost Password: 忘記密碼 +manage_rejudge: 重測管理 manage_dashboard: 概況 manage_domain: 管理域 manage_edit: 編輯域資料 diff --git a/packages/hydrooj/src/handler/manage.ts b/packages/hydrooj/src/handler/manage.ts index f87b70bb2..4c77ae0c5 100644 --- a/packages/hydrooj/src/handler/manage.ts +++ b/packages/hydrooj/src/handler/manage.ts @@ -354,6 +354,72 @@ class SystemUserPrivHandler extends SystemHandler { } } +class SystemRejudgeHandler extends SystemHandler { + async get() { + const rrdocs = await record.getMultiRejudgeTask({}); + this.response.body.rrdocs = rrdocs; + this.response.template = 'manage_rejudge.html'; + } + + @param('domainId', Types.String, true) + @param('pid', Types.Int, true) + @param('uid', Types.Int, true) + @param('contest', Types.String, true) + @param('lang', Types.String, true) + @param('status', Types.Int, true) + @param('apply', Types.Boolean) + async post(domainId: string, pid: number, uid: number, contest: string, lang: string, status: number, _apply = false) { + const rid = await record.add(domainId, -1, this.user._id, '-', 'rejudge', false, { + input: JSON.stringify({ + pid, uid, contest, lang, status, apply: _apply, + }), + type: 'rejudge', + }); + const args = global.Hydro.script['rejudge'].validate({ + rrid: rid.toHexString(), + domainId, + uid, + pid, + contest, + lang, + status, + apply: _apply, + }); + const report = (data) => judge.next({ domainId, rid, ...data }); + report({ message: 'Start rejudge', status: STATUS.STATUS_JUDGING }); + const start = Date.now(); + // Maybe async? + global.Hydro.script['rejudge'].run(args, report) + .then((ret: any) => { + const time = new Date().getTime() - start; + judge.end({ + domainId, + rid: rid.toHexString(), + status: STATUS.STATUS_ACCEPTED, + message: inspect(ret, false, 10, true), + judger: 1, + time, + memory: 0, + }); + }) + .catch((err: Error) => { + const time = new Date().getTime() - start; + logger.error(err); + judge.end({ + domainId, + rid: rid.toHexString(), + status: STATUS.STATUS_SYSTEM_ERROR, + message: `${err.message} \n${(err as any).params || []} \n${err.stack} `, + judger: 1, + time, + memory: 0, + }); + }); + this.response.body = { rid }; + this.response.redirect = this.url('record_detail', { rid }); + } +} + export const inject = ['config', 'check']; export async function apply(ctx) { ctx.Route('manage', '/manage', SystemMainHandler); @@ -363,5 +429,6 @@ export async function apply(ctx) { ctx.Route('manage_config', '/manage/config', SystemConfigHandler); ctx.Route('manage_user_import', '/manage/userimport', SystemUserImportHandler); ctx.Route('manage_user_priv', '/manage/userpriv', SystemUserPrivHandler); + ctx.Route('manage_rejudge', '/manage/rejudge', SystemRejudgeHandler); ctx.Connection('manage_check', '/manage/check-conn', SystemCheckConnHandler); } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 8935b795f..6a24c70fd 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -228,6 +228,25 @@ export interface RecordStatDoc { lang: string; } +export interface RecordRejudgeDoc { + _id: ObjectId; + owner: number; + domainId: string; + uid?: number; + pid?: number; + contest?: ObjectId; + lang?: string; + status?: number; + apply: boolean; + message: string; + finishAt: Date; + changes: { + rid: ObjectId; + old: number; + new: number; + }[]; +} + export interface ScoreboardNode { type: 'string' | 'rank' | 'user' | 'email' | 'record' | 'records' | 'problem' | 'solved' | 'time' | 'total_score'; value: string; // 显示分数 @@ -534,6 +553,7 @@ declare module './service/db' { 'record': RecordDoc; 'record.stat': RecordStatDoc; 'record.history': RecordHistoryDoc; + 'record.rejudge': RecordRejudgeDoc; 'document': any; 'document.status': StatusDocBase & { [K in keyof DocStatusType]: { docType: K } & DocStatusType[K]; diff --git a/packages/hydrooj/src/lib/ui.ts b/packages/hydrooj/src/lib/ui.ts index 741822ce3..71d7ec61f 100644 --- a/packages/hydrooj/src/lib/ui.ts +++ b/packages/hydrooj/src/lib/ui.ts @@ -63,6 +63,7 @@ inject('ControlPanel', 'manage_dashboard'); inject('ControlPanel', 'manage_script'); inject('ControlPanel', 'manage_user_import'); inject('ControlPanel', 'manage_user_priv'); +inject('ControlPanel', 'manage_bulk_rejudge'); inject('ControlPanel', 'manage_setting'); inject('ControlPanel', 'manage_config'); inject('DomainManage', 'domain_dashboard', { family: 'Properties', icon: 'info' }); diff --git a/packages/hydrooj/src/model/record.ts b/packages/hydrooj/src/model/record.ts index a1114a8fb..90bd9b463 100644 --- a/packages/hydrooj/src/model/record.ts +++ b/packages/hydrooj/src/model/record.ts @@ -8,7 +8,7 @@ import { import { ProblemConfigFile } from '@hydrooj/common'; import { Context } from '../context'; import { ProblemNotFoundError } from '../error'; -import { JudgeMeta, RecordDoc } from '../interface'; +import { JudgeMeta, RecordDoc, RecordRejudgeDoc } from '../interface'; import db from '../service/db'; import { MaybeArray, NumberKeys } from '../typeutils'; import { ArgMethod, buildProjection, Time } from '../utils'; @@ -21,6 +21,7 @@ export default class RecordModel { static coll = db.collection('record'); static collStat = db.collection('record.stat'); static collHistory = db.collection('record.history'); + static collRejudge = db.collection('record.rejudge'); static PROJECTION_LIST: (keyof RecordDoc)[] = [ '_id', 'score', 'time', 'memory', 'lang', 'uid', 'pid', 'rejudged', 'progress', 'domainId', @@ -280,6 +281,26 @@ export default class RecordModel { for (const rdoc of rdocs) r[rdoc._id.toHexString()] = rdoc; return r; } + + static async getMultiRejudgeTask(query: Filter) { + return RecordModel.collRejudge.find(query).toArray(); + } + + static async getRejudgeTask(_id: ObjectId) { + return RecordModel.collRejudge.findOne({ _id }); + } + + static async addRejudgeTask(doc: RecordRejudgeDoc) { + await RecordModel.collRejudge.insertOne(doc); + } + + static async pushRejudgeResult(rrid: ObjectId, result: { rid: ObjectId, old: number, new: number }) { + await RecordModel.collRejudge.updateOne({ _id: rrid }, { + $push: { + changes: result, + }, + }); + } } export async function apply(ctx: Context) { diff --git a/packages/hydrooj/src/script/rejudge.ts b/packages/hydrooj/src/script/rejudge.ts new file mode 100644 index 000000000..9b2927e1e --- /dev/null +++ b/packages/hydrooj/src/script/rejudge.ts @@ -0,0 +1,51 @@ +import { Filter, ObjectId } from 'mongodb'; +import Schema from 'schemastery'; +import { Context } from '../context'; +import { RecordDoc } from '../interface'; +import { STATUS, STATUS_SHORT_TEXTS } from '../model/builtin'; +import record from '../model/record'; + +export const apply = (ctx: Context) => ctx.addScript( + 'rejudge', 'rejudge with filter', + Schema.object({ + rrid: Schema.string(), + domainId: Schema.string(), + uid: Schema.number(), + pid: Schema.number(), + contest: Schema.string(), + lang: Schema.string(), + status: Schema.number(), + apply: Schema.boolean(), + }), + async (arg, report) => { + const q: Filter = { 'files.hack': { $exists: false } }; + for (const key of ['domainId', 'uid', 'pid', 'contest', 'lang', 'status']) { + if (arg[key]) q[key] = arg[key]; + } + q.contest ||= { $nin: [record.RECORD_GENERATE, record.RECORD_PRETEST] }; + q.status ||= { $ne: STATUS.STATUS_CANCELED }; + const rdocs = await record.getMulti(arg.domainId, q).project({ _id: 1, contest: 1, status: 1 }).toArray(); + const rdict = new Map(rdocs.map((rdoc) => [rdoc._id, rdoc.status])); + report({ message: `Found ${rdocs.length} records` }); + ctx.on('record/change', async (rdoc: RecordDoc) => { + if (rdict.has(rdoc._id)) { + rdict.delete(rdoc._id); + report({ message: `Rejudged ${rdoc._id}, ${STATUS_SHORT_TEXTS[rdict.get(rdoc._id)]} -> ${STATUS_SHORT_TEXTS[rdoc.status]}` }); + await record.pushRejudgeResult(new ObjectId(arg.rrid), { rid: rdoc._id, old: rdict.get(rdoc._id), new: rdoc.status }); + } + }); + if (rdocs.length) { + const priority = await record.submissionPriority(1, -10000 - rdocs.length * 5 - 50); + if (arg.apply) { + await record.reset(arg.domainId, rdocs.map((rdoc) => rdoc._id), true); + } + await Promise.all([ + record.judge(arg.domainId, rdocs.filter((i) => i.contest).map((i) => i._id), + priority, { detail: false }, { rejudge: arg.apply ? true : 'controlled' }), + record.judge(arg.domainId, rdocs.filter((i) => !i.contest).map((i) => i._id), + priority, {}, { rejudge: arg.apply ? true : 'controlled' }), + ]); + } + return true; + }, +); diff --git a/packages/ui-default/pages/manage_rejudge.page.ts b/packages/ui-default/pages/manage_rejudge.page.ts new file mode 100644 index 000000000..bccd18213 --- /dev/null +++ b/packages/ui-default/pages/manage_rejudge.page.ts @@ -0,0 +1,21 @@ +import $ from 'jquery'; +import CustomSelectAutoComplete from 'vj/components/autocomplete/CustomSelectAutoComplete'; +import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete'; +import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete'; +import { NamedPage } from 'vj/misc/Page'; + +const page = new NamedPage('manage_bulk_rejudge', async () => { + UserSelectAutoComplete.getOrConstruct($('[name="uidOrName"]'), { + clearDefaultValue: false, + }); + ProblemSelectAutoComplete.getOrConstruct($('[name="pid"]'), { + clearDefaultValue: false, + }); + const prefixes = new Set(Object.keys(window.LANGS).filter((i) => i.includes('.')).map((i) => i.split('.')[0])); + const langs = Object.keys(window.LANGS).filter((i) => !prefixes.has(i)).map((i) => ( + { name: `${i.includes('.') ? `${window.LANGS[i.split('.')[0]].display}/` : ''}${window.LANGS[i].display}`, _id: i } + )); + CustomSelectAutoComplete.getOrConstruct($('[name=lang]'), { multi: false, data: langs }); +}); + +export default page; diff --git a/packages/ui-default/templates/manage_rejudge.html b/packages/ui-default/templates/manage_rejudge.html new file mode 100644 index 000000000..77d060858 --- /dev/null +++ b/packages/ui-default/templates/manage_rejudge.html @@ -0,0 +1,63 @@ +{% import "components/user.html" as user %} +{% extends "manage_base.html" %} +{% block manage_content %} +
+
+

{{ _('Bulk Rejudge') }}

+
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ + +
+
+ +
+
+
+ +
+
+
+
+{% endblock %}