From 178c1cd359a2bf43b268c11fdfb98313ad7b68df Mon Sep 17 00:00:00 2001 From: panda Date: Thu, 17 Apr 2025 10:15:11 +0000 Subject: [PATCH 1/6] core&ui: support bulk rejudge task --- packages/hydrooj/locales/en.yaml | 1 + packages/hydrooj/locales/zh.yaml | 1 + packages/hydrooj/locales/zh_TW.yaml | 1 + packages/hydrooj/src/handler/manage.ts | 67 +++++++++++++++++++ packages/hydrooj/src/interface.ts | 20 ++++++ packages/hydrooj/src/lib/ui.ts | 1 + packages/hydrooj/src/model/record.ts | 23 ++++++- packages/hydrooj/src/script/rejudge.ts | 51 ++++++++++++++ .../ui-default/pages/manage_rejudge.page.ts | 21 ++++++ .../ui-default/templates/manage_rejudge.html | 63 +++++++++++++++++ 10 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 packages/hydrooj/src/script/rejudge.ts create mode 100644 packages/ui-default/pages/manage_rejudge.page.ts create mode 100644 packages/ui-default/templates/manage_rejudge.html diff --git a/packages/hydrooj/locales/en.yaml b/packages/hydrooj/locales/en.yaml index 62caaac292..118c63b7ba 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 988e63af5e..a4f0bf90a7 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 e5b87ed132..f4600da07b 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 f87b70bb28..4c77ae0c51 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 8935b795f4..6a24c70fda 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 741822ce32..71d7ec61ff 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 a1114a8fba..90bd9b4632 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 0000000000..9b2927e1e3 --- /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 0000000000..bccd182138 --- /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 0000000000..77d0608584 --- /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 %} From d1098b051e0ce8785cf33219b59dcb57c0f0b8af Mon Sep 17 00:00:00 2001 From: panda Date: Mon, 26 May 2025 06:29:21 +0000 Subject: [PATCH 2/6] fix page name --- packages/hydrooj/src/interface.ts | 9 ++------- packages/hydrooj/src/lib/ui.ts | 2 +- packages/ui-default/pages/manage_rejudge.page.ts | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 6a24c70fda..320f63f795 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -231,15 +231,10 @@ export interface RecordStatDoc { export interface RecordRejudgeDoc { _id: ObjectId; owner: number; - domainId: string; - uid?: number; - pid?: number; - contest?: ObjectId; - lang?: string; - status?: number; + reason: string; apply: boolean; - message: string; finishAt: Date; + rids: ObjectId[]; changes: { rid: ObjectId; old: number; diff --git a/packages/hydrooj/src/lib/ui.ts b/packages/hydrooj/src/lib/ui.ts index 71d7ec61ff..f433baf217 100644 --- a/packages/hydrooj/src/lib/ui.ts +++ b/packages/hydrooj/src/lib/ui.ts @@ -63,7 +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_rejudge'); inject('ControlPanel', 'manage_setting'); inject('ControlPanel', 'manage_config'); inject('DomainManage', 'domain_dashboard', { family: 'Properties', icon: 'info' }); diff --git a/packages/ui-default/pages/manage_rejudge.page.ts b/packages/ui-default/pages/manage_rejudge.page.ts index bccd182138..67353d1252 100644 --- a/packages/ui-default/pages/manage_rejudge.page.ts +++ b/packages/ui-default/pages/manage_rejudge.page.ts @@ -4,7 +4,7 @@ import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectA import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete'; import { NamedPage } from 'vj/misc/Page'; -const page = new NamedPage('manage_bulk_rejudge', async () => { +const page = new NamedPage('manage_rejudge', async () => { UserSelectAutoComplete.getOrConstruct($('[name="uidOrName"]'), { clearDefaultValue: false, }); From 39cc430969efa8d816e8eefa6b735c011971b7ff Mon Sep 17 00:00:00 2001 From: panda Date: Tue, 27 May 2025 10:05:46 +0000 Subject: [PATCH 3/6] add manage_rejudge --- .../ui-default/pages/manage_rejudge.page.ts | 5 +- .../ui-default/templates/manage_rejudge.html | 129 +++++++++++++++--- 2 files changed, 111 insertions(+), 23 deletions(-) diff --git a/packages/ui-default/pages/manage_rejudge.page.ts b/packages/ui-default/pages/manage_rejudge.page.ts index 67353d1252..f655f52023 100644 --- a/packages/ui-default/pages/manage_rejudge.page.ts +++ b/packages/ui-default/pages/manage_rejudge.page.ts @@ -1,3 +1,4 @@ +import { STATUS_TEXTS } from '@hydrooj/common'; import $ from 'jquery'; import CustomSelectAutoComplete from 'vj/components/autocomplete/CustomSelectAutoComplete'; import ProblemSelectAutoComplete from 'vj/components/autocomplete/ProblemSelectAutoComplete'; @@ -15,7 +16,9 @@ const page = new NamedPage('manage_rejudge', async () => { 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 }); + CustomSelectAutoComplete.getOrConstruct($('[name=lang]'), { multi: true, data: langs }); + const statuses = Object.values(STATUS_TEXTS).map((i) => ({ name: i, _id: i })); + CustomSelectAutoComplete.getOrConstruct($('[name=status]'), { multi: true, data: statuses }); }); export default page; diff --git a/packages/ui-default/templates/manage_rejudge.html b/packages/ui-default/templates/manage_rejudge.html index 77d0608584..b0dd0017e3 100644 --- a/packages/ui-default/templates/manage_rejudge.html +++ b/packages/ui-default/templates/manage_rejudge.html @@ -3,61 +3,146 @@ {% block manage_content %}
-

{{ _('Bulk Rejudge') }}

+

{{ _('Bulk Rejudge') }}

+ {% if error %} +

{{ error.split('\n')|join('
') }}

+ {% endif %}
-
+
-
+
-
-
-
+
-
+
-
+ {{ form.form_text({ + columns:3, + label:'Begin Date', + name:'beginAtDate', + placeholder:'YYYY-mm-dd', + value:dateBeginText, + date:true, + row:false + }) }} + {{ form.form_text({ + columns:2, + label:'Begin Time', + name:'beginAtTime', + placeholder:'HH:MM', + value:timeBeginText, + time:true, + row:false + }) }} + {{ form.form_text({ + columns:3, + label:'End Date', + name:'endAtDate', + placeholder:'YYYY-mm-dd', + value:dateEndText, + date:true, + row:false + }) }} + {{ form.form_text({ + columns:2, + label:'End Time', + name:'endAtTime', + placeholder:'HH:MM', + value:timeEndText, + time:true, + row:false + }) }} +
+
+
-
-
+
+
+
+ + +
+
-
+
+
+

{{ _('Bulk Rejudge Result') }}

+
+
+ + + + + + + + + + + + + + + + + + + {%- for item in tasks -%} + + + + + + + + + {%- endfor -%} + +
#{{ _('Operator') }}{{ _('Count') }}{{ _('Begin At') }}{{ _('Status') }}{{ _('Action') }}
{{ item._id.toHexString()|truncate(8,True,'') }}{{ user.render_inline(item.operator, badge=false) }}{{ item.count }}{{ item.beginAt }}{{ item.status }} + {{ _('View') }} +
+
+
{% endblock %} From f30f12ed338db83351351dd7d3ac59dab0ac3ef2 Mon Sep 17 00:00:00 2001 From: panda Date: Thu, 29 May 2025 10:08:32 +0000 Subject: [PATCH 4/6] add filter form --- packages/hydrooj/src/handler/manage.ts | 119 +++++++++++++++--- .../ui-default/pages/manage_rejudge.page.ts | 2 +- .../ui-default/templates/manage_rejudge.html | 26 ++-- 3 files changed, 114 insertions(+), 33 deletions(-) diff --git a/packages/hydrooj/src/handler/manage.ts b/packages/hydrooj/src/handler/manage.ts index 6e8a881369..d362e2d234 100644 --- a/packages/hydrooj/src/handler/manage.ts +++ b/packages/hydrooj/src/handler/manage.ts @@ -2,13 +2,19 @@ import { exec } from 'child_process'; import { inspect } from 'util'; import * as yaml from 'js-yaml'; import { omit } from 'lodash'; +import moment from 'moment'; +import { Filter, ObjectId } from 'mongodb'; import Schema from 'schemastery'; +import { Time } from '@hydrooj/utils'; import { - CannotEditSuperAdminError, NotLaunchedByPM2Error, UserNotFoundError, ValidationError, + CannotEditSuperAdminError, ContestNotFoundError, NotLaunchedByPM2Error, ProblemNotFoundError, RecordNotFoundError, UserNotFoundError, ValidationError, } from '../error'; +import { RecordDoc } from '../interface'; import { Logger } from '../logger'; -import { PRIV, STATUS } from '../model/builtin'; +import { NORMAL_STATUS, PRIV, STATUS } from '../model/builtin'; +import * as contest from '../model/contest'; import domain from '../model/domain'; +import problem from '../model/problem'; import record from '../model/record'; import * as setting from '../model/setting'; import * as system from '../model/system'; @@ -359,33 +365,99 @@ class SystemUserPrivHandler extends SystemHandler { class SystemRejudgeHandler extends SystemHandler { async get() { - const rrdocs = await record.getMultiRejudgeTask({}); - this.response.body.rrdocs = rrdocs; + this.response.body = { + rrdocs: await record.getMultiRejudgeTask({}), + apply: true, + status: NORMAL_STATUS.filter((i: STATUS) => ![STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_ACCEPTED].includes(i)).join(','), + }; 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('uidOrName', Types.UidOrName, true) + @param('pid', Types.ProblemId, true) + @param('tid', Types.ObjectId, true) + @param('langs', Types.CommaSeperatedArray, true) + @param('beginAtDate', Types.Date, true) + @param('beginAtTime', Types.Time, true) + @param('endAtDate', Types.Date, true) + @param('endAtTime', Types.Time, true) + @param('status', Types.CommaSeperatedArray, true) + @param('type', Types.Range(['preview', 'rejudge'])) + @param('high_priority', Types.Boolean) @param('apply', Types.Boolean) - async post(domainId: string, pid: number, uid: number, contest: string, lang: string, status: number, _apply = false) { + async post( + domainId: string, uidOrName?: string, pid?: string | number, tid?: ObjectId, + langs: string[] = [], beginAtDate?: string, beginAtTime?: string, endAtDate?: string, + endAtTime?: string, status: string[] = [], _type = 'rejudge', highPriority = false, _apply = false, + ) { + const q: Filter = {}; + if (uidOrName) { + const udoc = await user.getById(domainId, +uidOrName) + || await user.getByUname(domainId, uidOrName) + || await user.getByEmail(domainId, uidOrName); + if (udoc) q.uid = udoc._id; + else throw new UserNotFoundError(uidOrName); + } + if (tid) { + const tdoc = await contest.get(domainId, tid); + if (!tdoc) throw new ContestNotFoundError(domainId, tid); + q.contest = tdoc._id; + } + if (pid) { + const pdoc = await problem.get(domainId, pid); + if (pdoc) q.pid = pdoc.docId; + else throw new ProblemNotFoundError(domainId, pid); + } + if (langs.length) q.lang = { $in: langs.filter((i) => setting.langs[i]) }; + let beginAt = null; + let endAt = null; + if (beginAtDate) { + beginAt = moment(`${beginAtDate} ${beginAtTime || '00:00'}`); + if (!beginAt.isValid()) throw new ValidationError('beginAtDate', 'beginAtTime'); + q._id ||= {}; + q._id = { ...q._id, $gte: Time.getObjectID(beginAt) }; + } + if (endAtDate) { + endAt = moment(`${endAtDate} ${endAtTime || '23:59'}`); + if (!endAt.isValid()) throw new ValidationError('endAtDate', 'endAtTime'); + q._id ||= {}; + q._id = { ...q._id, $lte: Time.getObjectID(endAt) }; + } + if (beginAt && endAt && beginAt.isSameOrAfter(endAt)) throw new ValidationError('duration'); + const rids = await record.getMulti(domainId, q).project({ _id: 1 }).toArray(); + if (_type === 'preview') { + this.response.body = { + uidOrName, + pid, + tid, + langs: langs.join(','), + beginAtDate, + beginAtTime, + endAtDate, + endAtTime, + status: status.join(','), + highPriority, + apply: _apply, + recordLength: rids.length, + rrdocs: await record.getMultiRejudgeTask({}), + }; + this.response.template = 'manage_rejudge.html'; + return; + } const rid = await record.add(domainId, -1, this.user._id, '-', 'rejudge', false, { input: JSON.stringify({ - pid, uid, contest, lang, status, apply: _apply, + domainId, + rids: rids.map((i) => i._id.toString()), + highPriority, + apply: _apply, }), type: 'rejudge', }); const args = global.Hydro.script['rejudge'].validate({ rrid: rid.toHexString(), domainId, - uid, - pid, - contest, - lang, - status, + rids: rids.map((i) => i._id.toString()), + highPriority, apply: _apply, }); const report = (data) => judge.next({ domainId, rid, ...data }); @@ -419,7 +491,17 @@ class SystemRejudgeHandler extends SystemHandler { }); }); this.response.body = { rid }; - this.response.redirect = this.url('record_detail', { rid }); + this.response.redirect = this.url('manage_rejudge_detail', { rid: rid.toHexString() }); + } +} + +class SystemRejudgeDetailHandler extends SystemHandler { + @param('rid', Types.ObjectId) + async get(domainId: string, rid: ObjectId) { + const rrdoc = await record.getRejudgeTask(rid); + if (!rrdoc) throw new RecordNotFoundError(domainId, rid); + this.response.body = { rrdoc }; + this.response.template = 'manage_rejudge_detail.html'; } } @@ -433,5 +515,6 @@ export async function apply(ctx) { ctx.Route('manage_user_import', '/manage/userimport', SystemUserImportHandler); ctx.Route('manage_user_priv', '/manage/userpriv', SystemUserPrivHandler); ctx.Route('manage_rejudge', '/manage/rejudge', SystemRejudgeHandler); + ctx.Route('manage_rejudge_detail', '/manage/rejudge/:rid', SystemRejudgeDetailHandler); ctx.Connection('manage_check', '/manage/check-conn', SystemCheckConnHandler); } diff --git a/packages/ui-default/pages/manage_rejudge.page.ts b/packages/ui-default/pages/manage_rejudge.page.ts index f655f52023..d21a20c7ee 100644 --- a/packages/ui-default/pages/manage_rejudge.page.ts +++ b/packages/ui-default/pages/manage_rejudge.page.ts @@ -17,7 +17,7 @@ const page = new NamedPage('manage_rejudge', async () => { { name: `${i.includes('.') ? `${window.LANGS[i.split('.')[0]].display}/` : ''}${window.LANGS[i].display}`, _id: i } )); CustomSelectAutoComplete.getOrConstruct($('[name=lang]'), { multi: true, data: langs }); - const statuses = Object.values(STATUS_TEXTS).map((i) => ({ name: i, _id: i })); + const statuses = Object.entries(STATUS_TEXTS).map(([i, j]) => ({ name: j, _id: i })); CustomSelectAutoComplete.getOrConstruct($('[name=status]'), { multi: true, data: statuses }); }); diff --git a/packages/ui-default/templates/manage_rejudge.html b/packages/ui-default/templates/manage_rejudge.html index b0dd0017e3..886b14653a 100644 --- a/packages/ui-default/templates/manage_rejudge.html +++ b/packages/ui-default/templates/manage_rejudge.html @@ -6,10 +6,7 @@

{{ _('Bulk Rejudge') }}

- {% if error %} -

{{ error.split('\n')|join('
') }}

- {% endif %} -
+
{{ form.form_text({ - columns:3, + columns:4, label:'Begin Date', name:'beginAtDate', placeholder:'YYYY-mm-dd', - value:dateBeginText, + value:beginAtDate, date:true, row:false }) }} @@ -51,16 +48,16 @@

{{ _('Bulk Rejudge') }}

label:'Begin Time', name:'beginAtTime', placeholder:'HH:MM', - value:timeBeginText, + value:beginAtTime, time:true, row:false }) }} {{ form.form_text({ - columns:3, + columns:4, label:'End Date', name:'endAtDate', placeholder:'YYYY-mm-dd', - value:dateEndText, + value:endAtDate, date:true, row:false }) }} @@ -69,7 +66,7 @@

{{ _('Bulk Rejudge') }}

label:'End Time', name:'endAtTime', placeholder:'HH:MM', - value:timeEndText, + value:endAtTime, time:true, row:false }) }} @@ -95,14 +92,15 @@

{{ _('Bulk Rejudge') }}

+ {% if recordLength %} +

{{ _('Will rejudge {0} records').format(recordLength) }}

+ {% endif %}
- - + +
-
-
From 4c120db8de2c1733924031134e87299719ca054a Mon Sep 17 00:00:00 2001 From: panda Date: Thu, 29 May 2025 10:45:09 +0000 Subject: [PATCH 5/6] use record.judge --- packages/hydrooj/src/handler/manage.ts | 65 +++++++------------------- packages/hydrooj/src/interface.ts | 6 +-- packages/hydrooj/src/model/record.ts | 5 +- 3 files changed, 22 insertions(+), 54 deletions(-) diff --git a/packages/hydrooj/src/handler/manage.ts b/packages/hydrooj/src/handler/manage.ts index d362e2d234..ecc22cb8ab 100644 --- a/packages/hydrooj/src/handler/manage.ts +++ b/packages/hydrooj/src/handler/manage.ts @@ -7,7 +7,8 @@ import { Filter, ObjectId } from 'mongodb'; import Schema from 'schemastery'; import { Time } from '@hydrooj/utils'; import { - CannotEditSuperAdminError, ContestNotFoundError, NotLaunchedByPM2Error, ProblemNotFoundError, RecordNotFoundError, UserNotFoundError, ValidationError, + CannotEditSuperAdminError, ContestNotFoundError, NotLaunchedByPM2Error, ProblemNotFoundError, + RecordNotFoundError, UserNotFoundError, ValidationError, } from '../error'; import { RecordDoc } from '../interface'; import { Logger } from '../logger'; @@ -424,7 +425,7 @@ class SystemRejudgeHandler extends SystemHandler { q._id = { ...q._id, $lte: Time.getObjectID(endAt) }; } if (beginAt && endAt && beginAt.isSameOrAfter(endAt)) throw new ValidationError('duration'); - const rids = await record.getMulti(domainId, q).project({ _id: 1 }).toArray(); + const rdocs = await record.getMulti(domainId, q).project({ _id: 1, contest: 1 }).toArray(); if (_type === 'preview') { this.response.body = { uidOrName, @@ -438,60 +439,26 @@ class SystemRejudgeHandler extends SystemHandler { status: status.join(','), highPriority, apply: _apply, - recordLength: rids.length, + recordLength: rdocs.length, rrdocs: await record.getMultiRejudgeTask({}), }; this.response.template = 'manage_rejudge.html'; return; } - const rid = await record.add(domainId, -1, this.user._id, '-', 'rejudge', false, { - input: JSON.stringify({ - domainId, - rids: rids.map((i) => i._id.toString()), - highPriority, - apply: _apply, - }), - type: 'rejudge', - }); - const args = global.Hydro.script['rejudge'].validate({ - rrid: rid.toHexString(), - domainId, - rids: rids.map((i) => i._id.toString()), - highPriority, + const rid = await record.addRejudgeTask(domainId, { + owner: this.user._id, apply: _apply, + rids: rdocs.map((i) => i._id), }); - 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('manage_rejudge_detail', { rid: rid.toHexString() }); + const priority = await record.submissionPriority(this.user._id, -10000 - rdocs.length * 5 - 50); + await record.reset(domainId, rdocs.map((rdoc) => rdoc._id), true); + await Promise.all([ + record.judge(domainId, rdocs.filter((i) => i.contest).map((i) => i._id), priority, { detail: false }, + { rejudge: _apply ? true : 'controlled' }), + record.judge(domainId, rdocs.filter((i) => !i.contest).map((i) => i._id), priority, {}, + { rejudge: _apply ? true : 'controlled' }), + ]); + this.response.redirect = this.url('manage_rejudge_detail', { rid }); } } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 320f63f795..4dcc664700 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -230,12 +230,12 @@ export interface RecordStatDoc { export interface RecordRejudgeDoc { _id: ObjectId; + domainId: string; owner: number; - reason: string; apply: boolean; - finishAt: Date; + finishAt?: Date; rids: ObjectId[]; - changes: { + changes?: { rid: ObjectId; old: number; new: number; diff --git a/packages/hydrooj/src/model/record.ts b/packages/hydrooj/src/model/record.ts index 90bd9b4632..f68ea2b973 100644 --- a/packages/hydrooj/src/model/record.ts +++ b/packages/hydrooj/src/model/record.ts @@ -290,8 +290,9 @@ export default class RecordModel { return RecordModel.collRejudge.findOne({ _id }); } - static async addRejudgeTask(doc: RecordRejudgeDoc) { - await RecordModel.collRejudge.insertOne(doc); + static async addRejudgeTask(domainId: string, doc: Pick) { + const res = await RecordModel.collRejudge.insertOne({ _id: new ObjectId(), domainId, ...doc }); + return res.insertedId; } static async pushRejudgeResult(rrid: ObjectId, result: { rid: ObjectId, old: number, new: number }) { From 56897bf62292a7fe997f8fa39d11c347891d7759 Mon Sep 17 00:00:00 2001 From: panda Date: Fri, 30 May 2025 09:57:23 +0000 Subject: [PATCH 6/6] move to document --- packages/hydrooj/src/handler/manage.ts | 26 +++++++++---- packages/hydrooj/src/interface.ts | 28 ++++++++------ packages/hydrooj/src/model/document.ts | 5 ++- packages/hydrooj/src/model/record.ts | 28 +++++++++----- packages/hydrooj/src/script/rejudge.ts | 51 -------------------------- 5 files changed, 57 insertions(+), 81 deletions(-) delete mode 100644 packages/hydrooj/src/script/rejudge.ts diff --git a/packages/hydrooj/src/handler/manage.ts b/packages/hydrooj/src/handler/manage.ts index ecc22cb8ab..6f86f88e4d 100644 --- a/packages/hydrooj/src/handler/manage.ts +++ b/packages/hydrooj/src/handler/manage.ts @@ -1,7 +1,7 @@ import { exec } from 'child_process'; import { inspect } from 'util'; import * as yaml from 'js-yaml'; -import { omit } from 'lodash'; +import { omit, pick } from 'lodash'; import moment from 'moment'; import { Filter, ObjectId } from 'mongodb'; import Schema from 'schemastery'; @@ -367,7 +367,7 @@ class SystemUserPrivHandler extends SystemHandler { class SystemRejudgeHandler extends SystemHandler { async get() { this.response.body = { - rrdocs: await record.getMultiRejudgeTask({}), + rrdocs: await record.getMultiRejudgeTask(undefined, {}), apply: true, status: NORMAL_STATUS.filter((i: STATUS) => ![STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_ACCEPTED].includes(i)).join(','), }; @@ -440,7 +440,7 @@ class SystemRejudgeHandler extends SystemHandler { highPriority, apply: _apply, recordLength: rdocs.length, - rrdocs: await record.getMultiRejudgeTask({}), + rrdocs: await record.getMultiRejudgeTask(undefined, {}), }; this.response.template = 'manage_rejudge.html'; return; @@ -448,10 +448,19 @@ class SystemRejudgeHandler extends SystemHandler { const rid = await record.addRejudgeTask(domainId, { owner: this.user._id, apply: _apply, - rids: rdocs.map((i) => i._id), }); - const priority = await record.submissionPriority(this.user._id, -10000 - rdocs.length * 5 - 50); - await record.reset(domainId, rdocs.map((rdoc) => rdoc._id), true); + const priority = await record.submissionPriority(this.user._id, (highPriority ? 0 : -10000) - rdocs.length * 5 - 50); + if (_apply) await record.reset(domainId, rdocs.map((rdoc) => rdoc._id), true); + else { + await record.collHistory.insertMany(rdocs.map((rdoc) => ({ + ...pick(rdoc, [ + 'compilerTexts', 'judgeTexts', 'testCases', 'subtasks', + 'score', 'time', 'memory', 'status', 'judgeAt', 'judger', + ]), + rid: rdoc._id, + _id: new ObjectId(), + }))); + } await Promise.all([ record.judge(domainId, rdocs.filter((i) => i.contest).map((i) => i._id), priority, { detail: false }, { rejudge: _apply ? true : 'controlled' }), @@ -465,9 +474,10 @@ class SystemRejudgeHandler extends SystemHandler { class SystemRejudgeDetailHandler extends SystemHandler { @param('rid', Types.ObjectId) async get(domainId: string, rid: ObjectId) { - const rrdoc = await record.getRejudgeTask(rid); + const rrdoc = await record.getRejudgeTask(domainId, rid); + const rdocs = await record.getMulti(domainId, { _id: { $in: rrdoc.rids } }).toArray(); if (!rrdoc) throw new RecordNotFoundError(domainId, rid); - this.response.body = { rrdoc }; + this.response.body = { rrdoc, rdocs }; this.response.template = 'manage_rejudge_detail.html'; } } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 4dcc664700..6dea125973 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -228,18 +228,24 @@ export interface RecordStatDoc { lang: string; } -export interface RecordRejudgeDoc { - _id: ObjectId; - domainId: string; - owner: number; +export interface RecordRejudgeDoc extends Document { + content: string; + docId: ObjectId; + docType: document['TYPE_REJUDGE']; apply: boolean; finishAt?: Date; - rids: ObjectId[]; - changes?: { - rid: ObjectId; - old: number; - new: number; - }[]; +} + +export interface RecordRejudgeResultDoc { + _id: ObjectId; + rrid: ObjectId; + rid: ObjectId; + oldRev: ObjectId; + newRev: ObjectId; + oldStatus: number; + newStatus: number; + oldScore: number; + newScore: number; } export interface ScoreboardNode { @@ -548,7 +554,7 @@ declare module './service/db' { 'record': RecordDoc; 'record.stat': RecordStatDoc; 'record.history': RecordHistoryDoc; - 'record.rejudge': RecordRejudgeDoc; + 'record.rejudge': RecordRejudgeResultDoc; 'document': any; 'document.status': StatusDocBase & { [K in keyof DocStatusType]: { docType: K } & DocStatusType[K]; diff --git a/packages/hydrooj/src/model/document.ts b/packages/hydrooj/src/model/document.ts index d076b0a26a..44fcaa55b8 100644 --- a/packages/hydrooj/src/model/document.ts +++ b/packages/hydrooj/src/model/document.ts @@ -7,7 +7,7 @@ import { Context } from '../context'; import { Content, ContestClarificationDoc, DiscussionDoc, DiscussionReplyDoc, ProblemDoc, ProblemStatusDoc, - Tdoc, TrainingDoc, + RecordRejudgeDoc, Tdoc, TrainingDoc, } from '../interface'; import bus from '../service/bus'; import db from '../service/db'; @@ -29,6 +29,7 @@ export const TYPE_DISCUSSION_REPLY = 22 as const; export const TYPE_CONTEST = 30 as const; export const TYPE_CONTEST_CLARIFICATION = 31 as const; export const TYPE_TRAINING = 40 as const; +export const TYPE_REJUDGE = 50 as const; export interface DocType { [TYPE_PROBLEM]: ProblemDoc; @@ -40,6 +41,7 @@ export interface DocType { [TYPE_CONTEST]: Tdoc; [TYPE_CONTEST_CLARIFICATION]: ContestClarificationDoc; [TYPE_TRAINING]: TrainingDoc; + [TYPE_REJUDGE]: RecordRejudgeDoc; } export interface DocStatusType { @@ -487,5 +489,6 @@ global.Hydro.model.document = { TYPE_PROBLEM, TYPE_PROBLEM_LIST, TYPE_PROBLEM_SOLUTION, + TYPE_REJUDGE, TYPE_TRAINING, }; diff --git a/packages/hydrooj/src/model/record.ts b/packages/hydrooj/src/model/record.ts index f68ea2b973..4572846de8 100644 --- a/packages/hydrooj/src/model/record.ts +++ b/packages/hydrooj/src/model/record.ts @@ -13,6 +13,7 @@ import db from '../service/db'; import { MaybeArray, NumberKeys } from '../typeutils'; import { ArgMethod, buildProjection, Time } from '../utils'; import { STATUS } from './builtin'; +import * as document from './document'; import DomainModel from './domain'; import problem from './problem'; import task from './task'; @@ -282,24 +283,27 @@ export default class RecordModel { return r; } - static async getMultiRejudgeTask(query: Filter) { - return RecordModel.collRejudge.find(query).toArray(); + static async getMultiRejudgeTask(domainId: string | undefined, query: Filter) { + return document.getMulti(domainId, document.TYPE_REJUDGE, query).toArray(); } - static async getRejudgeTask(_id: ObjectId) { - return RecordModel.collRejudge.findOne({ _id }); + static async getRejudgeTask(domainId: string, _id: ObjectId) { + return document.get(domainId, document.TYPE_REJUDGE, _id); } - static async addRejudgeTask(domainId: string, doc: Pick) { - const res = await RecordModel.collRejudge.insertOne({ _id: new ObjectId(), domainId, ...doc }); - return res.insertedId; + static async addRejudgeTask(domainId: string, doc: Partial) { + return await document.add(domainId, '', doc.owner, document.TYPE_REJUDGE, null, null, null, doc); } - static async pushRejudgeResult(rrid: ObjectId, result: { rid: ObjectId, old: number, new: number }) { + static async pushRejudgeResult(rrid: ObjectId, newStatus: number, newScore: number, newRev) { await RecordModel.collRejudge.updateOne({ _id: rrid }, { - $push: { - changes: result, + $set: { + newStatus, + newScore, + newRev, }, + }, { + upsert: true, }); } } @@ -349,6 +353,10 @@ export async function apply(ctx: Context) { RecordModel.collHistory, { key: { rid: 1, _id: -1 }, name: 'basic' }, ), + db.ensureIndexes( + RecordModel.collRejudge, + { key: { domainId: 1, rrid: 1, rid: 1 }, name: 'basic' }, + ), ]); } global.Hydro.model.record = RecordModel; diff --git a/packages/hydrooj/src/script/rejudge.ts b/packages/hydrooj/src/script/rejudge.ts deleted file mode 100644 index 9b2927e1e3..0000000000 --- a/packages/hydrooj/src/script/rejudge.ts +++ /dev/null @@ -1,51 +0,0 @@ -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; - }, -);