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 8d6a71d20c..6f86f88e4d 100644 --- a/packages/hydrooj/src/handler/manage.ts +++ b/packages/hydrooj/src/handler/manage.ts @@ -1,14 +1,21 @@ 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'; +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'; @@ -357,6 +364,124 @@ class SystemUserPrivHandler extends SystemHandler { } } +class SystemRejudgeHandler extends SystemHandler { + async get() { + this.response.body = { + rrdocs: await record.getMultiRejudgeTask(undefined, {}), + 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('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, 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 rdocs = await record.getMulti(domainId, q).project({ _id: 1, contest: 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: rdocs.length, + rrdocs: await record.getMultiRejudgeTask(undefined, {}), + }; + this.response.template = 'manage_rejudge.html'; + return; + } + const rid = await record.addRejudgeTask(domainId, { + owner: this.user._id, + apply: _apply, + }); + 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' }), + 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 }); + } +} + +class SystemRejudgeDetailHandler extends SystemHandler { + @param('rid', Types.ObjectId) + async get(domainId: string, rid: ObjectId) { + 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, rdocs }; + this.response.template = 'manage_rejudge_detail.html'; + } +} + export const inject = ['config', 'check']; export async function apply(ctx) { ctx.Route('manage', '/manage', SystemMainHandler); @@ -366,5 +491,7 @@ 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.Route('manage_rejudge_detail', '/manage/rejudge/:rid', SystemRejudgeDetailHandler); ctx.Connection('manage_check', '/manage/check-conn', SystemCheckConnHandler); } diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 8935b795f4..6dea125973 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -228,6 +228,26 @@ export interface RecordStatDoc { lang: string; } +export interface RecordRejudgeDoc extends Document { + content: string; + docId: ObjectId; + docType: document['TYPE_REJUDGE']; + apply: boolean; + finishAt?: Date; +} + +export interface RecordRejudgeResultDoc { + _id: ObjectId; + rrid: ObjectId; + rid: ObjectId; + oldRev: ObjectId; + newRev: ObjectId; + oldStatus: number; + newStatus: number; + oldScore: number; + newScore: number; +} + export interface ScoreboardNode { type: 'string' | 'rank' | 'user' | 'email' | 'record' | 'records' | 'problem' | 'solved' | 'time' | 'total_score'; value: string; // 显示分数 @@ -534,6 +554,7 @@ declare module './service/db' { 'record': RecordDoc; 'record.stat': RecordStatDoc; 'record.history': RecordHistoryDoc; + 'record.rejudge': RecordRejudgeResultDoc; '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..f433baf217 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_rejudge'); inject('ControlPanel', 'manage_setting'); inject('ControlPanel', 'manage_config'); inject('DomainManage', 'domain_dashboard', { family: 'Properties', icon: 'info' }); 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 a1114a8fba..4572846de8 100644 --- a/packages/hydrooj/src/model/record.ts +++ b/packages/hydrooj/src/model/record.ts @@ -8,11 +8,12 @@ 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'; import { STATUS } from './builtin'; +import * as document from './document'; import DomainModel from './domain'; import problem from './problem'; import task from './task'; @@ -21,6 +22,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 +282,30 @@ export default class RecordModel { for (const rdoc of rdocs) r[rdoc._id.toHexString()] = rdoc; return r; } + + static async getMultiRejudgeTask(domainId: string | undefined, query: Filter) { + return document.getMulti(domainId, document.TYPE_REJUDGE, query).toArray(); + } + + static async getRejudgeTask(domainId: string, _id: ObjectId) { + return document.get(domainId, document.TYPE_REJUDGE, _id); + } + + 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, newStatus: number, newScore: number, newRev) { + await RecordModel.collRejudge.updateOne({ _id: rrid }, { + $set: { + newStatus, + newScore, + newRev, + }, + }, { + upsert: true, + }); + } } export async function apply(ctx: Context) { @@ -327,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/ui-default/pages/manage_rejudge.page.ts b/packages/ui-default/pages/manage_rejudge.page.ts new file mode 100644 index 0000000000..d21a20c7ee --- /dev/null +++ b/packages/ui-default/pages/manage_rejudge.page.ts @@ -0,0 +1,24 @@ +import { STATUS_TEXTS } from '@hydrooj/common'; +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_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: true, data: langs }); + const statuses = Object.entries(STATUS_TEXTS).map(([i, j]) => ({ name: j, _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 new file mode 100644 index 0000000000..886b14653a --- /dev/null +++ b/packages/ui-default/templates/manage_rejudge.html @@ -0,0 +1,146 @@ +{% import "components/user.html" as user %} +{% extends "manage_base.html" %} +{% block manage_content %} +
+
+

{{ _('Bulk Rejudge') }}

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

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

+ {% endif %} +
+
+ + +
+
+
+
+
+
+
+

{{ _('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 %}