From 7a41fecb0ed8cc578c1745f390c87bd2f5281ff0 Mon Sep 17 00:00:00 2001 From: undefined Date: Sun, 7 Mar 2021 14:25:16 +0800 Subject: [PATCH] =?UTF-8?q?core:=20=E6=95=B4=E5=90=88tag=E4=B8=8Ecategory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/fps-importer/handler.ts | 2 +- packages/fps-importer/package.json | 4 +- packages/hydrooj/package.json | 2 +- packages/hydrooj/src/handler/import.syzoj.ts | 6 +- packages/hydrooj/src/handler/problem.ts | 110 ++++++------------- packages/hydrooj/src/handler/remote.ts | 2 +- packages/hydrooj/src/interface.ts | 3 +- packages/hydrooj/src/logger.ts | 42 ++++++- packages/hydrooj/src/model/document.ts | 2 - packages/hydrooj/src/model/problem.ts | 7 +- packages/hydrooj/src/service/bus.ts | 7 +- packages/hydrooj/src/ui.ts | 1 - packages/hydrooj/src/upgrade.ts | 34 +++++- packages/migrate-hustoj/script.ts | 2 +- packages/syzoj-ng-api/handlers/problem.ts | 9 +- 15 files changed, 116 insertions(+), 117 deletions(-) diff --git a/packages/fps-importer/handler.ts b/packages/fps-importer/handler.ts index a5bc52bb9..dbca57d1e 100644 --- a/packages/fps-importer/handler.ts +++ b/packages/fps-importer/handler.ts @@ -35,7 +35,7 @@ class FpsProblemImportHandler extends Handler { }; const title = decodeHTML(p.title.join(' ')); const tags = filter(p.source, (i: string) => i.trim()); - const pid = await problem.add(domainId, null, title, content, this.user._id, tags, []); + const pid = await problem.add(domainId, null, title, content, this.user._id, tags); await problem.addTestdata(domainId, pid, 'config.yaml', Buffer.from(`time: ${config.time}\nmemory: ${config.memory}`)); if (p.test_output) { for (let i = 0; i < p.test_input.length; i++) { diff --git a/packages/fps-importer/package.json b/packages/fps-importer/package.json index f3b99ed30..8666d9926 100644 --- a/packages/fps-importer/package.json +++ b/packages/fps-importer/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/fps-importer", - "version": "1.1.7", + "version": "1.1.8", "description": "Import FPS problems", "main": "package.json", "repository": "https://github.com/hydro-dev/Hydro.git", @@ -17,7 +17,7 @@ "bson": "^4.2.2" }, "peerDependencies": { - "hydrooj": "^2.15.0" + "hydrooj": "^2.20.0" }, "scripts": { "build": "tsc", diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index d12e732c9..c69bac01c 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.19.6", + "version": "2.20.0", "bin": "bin/hydrooj.js", "main": "dist/loader.js", "typings": "dist/loader.d.ts", diff --git a/packages/hydrooj/src/handler/import.syzoj.ts b/packages/hydrooj/src/handler/import.syzoj.ts index 673cd2679..c953d9d74 100644 --- a/packages/hydrooj/src/handler/import.syzoj.ts +++ b/packages/hydrooj/src/handler/import.syzoj.ts @@ -96,8 +96,7 @@ class ProblemImportSYZOJHandler extends Handler { }); } const docId = await problem.add( - domainId, target, p.title, content, this.user._id, - p.tags || [], [], hidden, + domainId, target, p.title, content, this.user._id, p.tags || [], hidden, ); const r = download(`${url}testdata/download`); const file = path.resolve(os.tmpdir(), 'hydro', `import_${domainId}_${docId}.zip`); @@ -166,8 +165,7 @@ class ProblemImportSYZOJHandler extends Handler { ...result.body.localizedContentsOfAllLocales, ][0].title; const docId = await problem.add( - domainId, target, title, content, this.user._id, - tags || [], [], hidden, + domainId, target, title, content, this.user._id, tags || [], hidden, ); const syncFiles = async () => { const judge = result.body.judgeInfo; diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index a9a5094b1..fa75169cd 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -1,10 +1,9 @@ import { isSafeInteger, flatten } from 'lodash'; -import yaml from 'js-yaml'; import { FilterQuery, ObjectID } from 'mongodb'; import AdmZip from 'adm-zip'; import { NoProblemError, PermissionError, ValidationError, - SolutionNotFoundError, ProblemNotFoundError, + SolutionNotFoundError, ProblemNotFoundError, BadRequestError, } from '../error'; import { Pdoc, User, Rdoc, PathComponent, @@ -102,14 +101,7 @@ export class ProblemCategoryHandler extends ProblemHandler { async get(domainId: string, page = 1, category: string[]) { this.response.template = 'problem_main.html'; const q: any = { $and: [] }; - for (const name of category) { - q.$and.push({ - $or: [ - { category: { $elemMatch: { $eq: name } } }, - { tag: { $elemMatch: { $eq: name } } }, - ], - }); - } + for (const tag of category) q.$and.push({ tag }); let psdict = {}; if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; await bus.serial('problem/list', q, this); @@ -138,17 +130,8 @@ export class ProblemCategoryHandler extends ProblemHandler { export class ProblemRandomHandler extends ProblemHandler { @param('category', Types.String, true, null, parseCategory) async get(domainId: string, category: string[] = []) { - const q: any = category.length ? { $and: [] } : {}; - for (const name of category) { - if (name) { - q.$and.push({ - $or: [ - { category: { $elemMatch: { $eq: name } } }, - { tag: { $elemMatch: { $eq: name } } }, - ], - }); - } - } + const q: FilterQuery = category.length ? { $and: [] } : {}; + for (const tag of category) q.$and.push({ tag }); if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; await bus.serial('problem/list', q, this); const pid = await problem.random(domainId, q); @@ -321,45 +304,6 @@ export class ProblemManageHandler extends ProblemDetailHandler { } } -export class ProblemSettingsHandler extends ProblemManageHandler { - @param('pid', Types.String) - async get(domainId: string, pid: string) { - this.response.template = 'problem_settings.html'; - this.response.body.path = [ - ['Hydro', 'homepage'], - ['problem_main', 'problem_main'], - [this.pdoc.title, 'problem_detail', { pid }, true], - ['problem_settings', null], - ]; - } - - @param('pid', Types.String, null, parsePid) - @param('yaml', Types.String) - async postConfig(domainId: string, pid: string | number, cfg: string) { - const pdoc = await problem.get(domainId, pid); - // TODO validate - const config = yaml.load(cfg) as any; - await problem.edit(domainId, pdoc.docId, { config }); - this.back(); - } - - @param('pid', Types.String, null, parsePid) - @param('hidden', Types.Boolean) - @param('category', Types.String, true, null, parseCategory) - @param('tag', Types.String, true, null, parseCategory) - async postSetting( - domainId: string, pid: string | number, hidden = false, - category: string[] = [], tag: string[] = [], - ) { - const pdoc = await problem.get(domainId, pid); - const update: Partial = { hidden, category, tag }; - await bus.serial('problem/setting', update, this); - await problem.edit(domainId, pdoc.docId, update); - await global.Hydro.script.difficulty.run({ domainId, pid }, console.log); - this.back(); - } -} - export class ProblemEditHandler extends ProblemManageHandler { async get({ pid }) { this.response.template = 'problem_edit.html'; @@ -371,17 +315,29 @@ export class ProblemEditHandler extends ProblemManageHandler { ]; } - @param('title', Types.String, isTitle) - @param('content', Types.String, isContent) + @route('pid', Types.String, null, parsePid) + @post('title', Types.String, isTitle) + @post('content', Types.String, isContent) @post('pid', Types.String, isPid, true) - async post(domainId: string, title: string, content: string, newPid: string = '') { + @post('hidden', Types.Boolean) + @post('tag', Types.String, true, null, parseCategory) + async post( + domainId: string, pid: string | number, title: string, content: string, + newPid: string = '', hidden = false, tag: string[], + ) { try { content = JSON.parse(content); } catch { /* Ignore */ } - const $update: Partial = { title, content, pid: newPid }; - let pdoc = await problem.get(domainId, this.request.params.pid); + if (await problem.get(domainId, newPid)) throw new BadRequestError('new pid exists'); + const $update: Partial = { + title, content, pid: newPid, hidden, tag, + }; + let pdoc = await problem.get(domainId, pid); + await bus.serial('problem/before-edit', $update); pdoc = await problem.edit(domainId, pdoc.docId, $update); - this.response.redirect = this.url('problem_detail', { pid: pdoc.pid || pdoc.docId }); + await bus.emit('problem/edit', pdoc); + await global.Hydro.script.difficulty.run({ domainId, pid }, console.log); + this.response.redirect = this.url('problem_detail', { pid: newPid || pdoc.docId }); } } @@ -594,20 +550,19 @@ export class ProblemCreateHandler extends Handler { }; } - @param('title', Types.String, isTitle) - @param('pid', Types.String, isPid, true) - @param('content', Types.String, isContent) - @param('hidden', Types.Boolean) - async post(domainId: string, title: string, pid: string, content: string, hidden = false) { + @post('title', Types.String, isTitle) + @post('content', Types.String, isContent) + @post('pid', Types.String, isPid, true) + @post('hidden', Types.Boolean) + @post('tag', Types.String, true, null, parseCategory) + async post(domainId: string, title: string, content: string, pid: string, hidden = false, tag: string[] = []) { try { content = JSON.parse(content); } catch { /* Ignore */ } - const docId = await problem.add( - domainId, pid, title, content, - this.user._id, [], [], hidden, - ); - this.response.body = { pid: docId }; - this.response.redirect = this.url('problem_settings', { pid: docId }); + if (await problem.get(domainId, pid)) throw new BadRequestError('invalid pid'); + const docId = await problem.add(domainId, pid, title, content, this.user._id, tag, hidden); + this.response.body = { pid: pid || docId }; + this.response.redirect = this.url('problem_settings', { pid: pid || docId }); } } @@ -618,7 +573,6 @@ export async function apply() { Route('problem_detail', '/p/:pid', ProblemDetailHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_submit', '/p/:pid/submit', ProblemSubmitHandler, PERM.PERM_SUBMIT_PROBLEM); Route('problem_pretest', '/p/:pid/pretest', ProblemPretestHandler, PERM.PERM_SUBMIT_PROBLEM); - Route('problem_settings', '/p/:pid/settings', ProblemSettingsHandler); Route('problem_statistics', '/p/:pid/statistics', ProblemStatisticsHandler, PERM.PERM_VIEW_PROBLEM); Route('problem_edit', '/p/:pid/edit', ProblemEditHandler); Route('problem_files', '/p/:pid/files', ProblemFilesHandler, PERM.PERM_VIEW_PROBLEM); diff --git a/packages/hydrooj/src/handler/remote.ts b/packages/hydrooj/src/handler/remote.ts index ad7976c96..d94ea2429 100644 --- a/packages/hydrooj/src/handler/remote.ts +++ b/packages/hydrooj/src/handler/remote.ts @@ -141,7 +141,7 @@ export class ProblemReceiveHandler extends Handler { const exist = await problem.get(domainId, pdoc.pid); if (exist) pdoc.pid = undefined; } - const pid = await problem.add(domainId, pdoc.pid, pdoc.title, pdoc.content, this.user._id, pdoc.tag, pdoc.category); + const pid = await problem.add(domainId, pdoc.pid, pdoc.title, pdoc.content, this.user._id, pdoc.tag); pids.push(pid); this.syncFiles(domainId, baseUrl, pid, files[i]).catch(logAndReturn(logger)); await problem.edit(domainId, pid, { html: pdoc.html }); diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index f42897618..49bee8d6b 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -174,7 +174,8 @@ declare module './model/problem' { nSubmit: number, nAccept: number, tag: string[], - category: string[], + /** @deprecated */ + category?: string[], data: FileInfo[], additional_file: FileInfo[], hidden: boolean, diff --git a/packages/hydrooj/src/logger.ts b/packages/hydrooj/src/logger.ts index 77d46dd27..7c3bce238 100644 --- a/packages/hydrooj/src/logger.ts +++ b/packages/hydrooj/src/logger.ts @@ -1,11 +1,19 @@ -import { inspect, format } from 'util'; +import { inspect, format, InspectOptions } from 'util'; import { argv } from 'yargs'; import { Time } from './utils'; +const colors = [ + 20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62, + 63, 68, 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113, + 129, 134, 135, 148, 149, 160, 161, 162, 163, 164, 165, 166, 167, 168, + 169, 170, 171, 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200, + 201, 202, 203, 204, 205, 206, 207, 208, 209, 214, 215, 220, 221, +]; const instances: Record = {}; -type LogFunction = (format: any, ...param: any[]) => boolean; +type LogFunction = (format: any, ...param: any[]) => void; type LogType = 'success' | 'error' | 'info' | 'warn' | 'debug'; export interface Logger extends Record { } + export class Logger { static readonly SUCCESS = 1; static readonly ERROR = 1; @@ -16,10 +24,24 @@ export class Logger { static showDiff = false; static levels: Record = {}; static lastTime = 0; - static formatters: Record string> = { - o: (value) => inspect(value).replace(/\s*\n\s*/g, ' '), + private code: number; + private displayName: string; + public stream: NodeJS.WritableStream = process.stderr; + static options: InspectOptions = { + colors: process.stderr.isTTY, }; + static formatters: Record string> = { + c: Logger.prototype.color, + C: (value) => Logger.color(15, value, ';1'), + o: (value) => inspect(value, Logger.options).replace(/\s*\n\s*/g, ' '), + }; + + static color(code: number, value: any, decoration = '') { + if (!Logger.options.colors) return `${value}`; + return `\u001B[3${code < 8 ? code : `8;5;${code}`}${decoration}m${value}\u001B[0m`; + } + constructor(public name: string, private showDiff = false) { if (name in instances) return instances[name]; let hash = 0; @@ -28,6 +50,8 @@ export class Logger { hash |= 0; } instances[name] = this; + this.code = colors[Math.abs(hash) % colors.length]; + this.displayName = name ? this.color(`${name} `, ';1') : ''; this.createMethod('success', '[S] ', Logger.SUCCESS); this.createMethod('error', '[E] ', Logger.ERROR); this.createMethod('info', '[I] ', Logger.INFO); @@ -35,10 +59,14 @@ export class Logger { this.createMethod('debug', '[D] ', Logger.DEBUG); } + private color(value: any, decoration = '') { + return Logger.color(this.code, value, decoration); + } + private createMethod(name: LogType, prefix: string, minLevel: number) { this[name] = (...args: [any, ...any[]]) => { if (this.level < minLevel) return false; - const msg = `${prefix} ${this.name} ${this.format(...args)}`; + const msg = `${prefix} ${this.displayName} ${this.format(...args)}`; if (process.send) process.send({ event: 'message/log', payload: [msg] }); else global.Hydro.service.bus.parallel('message/log', msg); return true; @@ -68,7 +96,9 @@ export class Logger { }).split('\n').join('\n '); if (Logger.showDiff || this.showDiff) { const now = Date.now(); - if (Logger.lastTime) args.push(`+${Time.formatTimeShort(now - Logger.lastTime)}`); + if (Logger.lastTime) { + args.push(this.color(`+${Time.formatTimeShort(now - Logger.lastTime)}`)); + } Logger.lastTime = now; } return format(...args); diff --git a/packages/hydrooj/src/model/document.ts b/packages/hydrooj/src/model/document.ts index 5331286d5..9a8828af9 100644 --- a/packages/hydrooj/src/model/document.ts +++ b/packages/hydrooj/src/model/document.ts @@ -390,8 +390,6 @@ async function ensureIndexes() { await ic({ domainId: 1, docType: 1, owner: 1, docId: -1 }); // For problem await ic({ domainId: 1, docType: 1, search: 'text', title: 'text' }, s); - await ic({ domainId: 1, docType: 1, category: 1, docId: 1 }, s); - await ic({ domainId: 1, docType: 1, hidden: 1, category: 1, docId: 1 }, s); await ic({ domainId: 1, docType: 1, tag: 1, docId: 1 }, s); await ic({ domainId: 1, docType: 1, hidden: 1, tag: 1, docId: 1 }, s); // For problem solution diff --git a/packages/hydrooj/src/model/problem.ts b/packages/hydrooj/src/model/problem.ts index 0a6317187..21ca8ada9 100644 --- a/packages/hydrooj/src/model/problem.ts +++ b/packages/hydrooj/src/model/problem.ts @@ -38,7 +38,6 @@ export namespace Pdoc { nSubmit: 0, nAccept: 0, tag: [], - category: [], data: [], additional_file: [], hidden: true, @@ -72,7 +71,7 @@ export const SETTING_DIFFICULTY_RANGE = [ export const PROJECTION_LIST: Pdoc.Field[] = [ '_id', 'domainId', 'docType', 'docId', 'pid', 'owner', 'title', 'nSubmit', 'nAccept', 'difficulty', - 'tag', 'category', 'hidden', + 'tag', 'hidden', ]; export const PROJECTION_PUBLIC: Pdoc.Field[] = [ @@ -83,11 +82,11 @@ export const PROJECTION_PUBLIC: Pdoc.Field[] = [ export async function add( domainId: string, pid: string = null, title: string, content: Content, owner: number, - tag: string[] = [], category: string[] = [], hidden = false, + tag: string[] = [], hidden = false, ) { const pidCounter = await domain.inc(domainId, 'pidCounter', 1); const args: Partial = { - title, category, tag, hidden, nSubmit: 0, nAccept: 0, + title, tag, hidden, nSubmit: 0, nAccept: 0, }; if (pid) args.pid = pid; return await document.add(domainId, content, owner, document.TYPE_PROBLEM, pidCounter, null, null, args); diff --git a/packages/hydrooj/src/service/bus.ts b/packages/hydrooj/src/service/bus.ts index 0a632d842..63cf3ae3d 100644 --- a/packages/hydrooj/src/service/bus.ts +++ b/packages/hydrooj/src/service/bus.ts @@ -52,9 +52,12 @@ export interface EventMap { 'domain/update': (domainId: string, $set: Partial, ddoc: DomainDoc) => VoidReturn 'document/add': (doc: any) => VoidReturn - 'document/set': - (domainId: string, docType: T, docId: DocType[T], $set: any, $unset: OnlyFieldsOfType) => VoidReturn + 'document/set': ( + domainId: string, docType: T, docId: DocType[T], + $set: any, $unset: OnlyFieldsOfType + ) => VoidReturn + 'problem/before-edit': (doc: Partial) => VoidReturn 'problem/edit': (doc: Pdoc) => VoidReturn 'problem/list': (query: FilterQuery, handler: any) => VoidReturn 'problem/setting': (update: Partial, handler: any) => VoidReturn diff --git a/packages/hydrooj/src/ui.ts b/packages/hydrooj/src/ui.ts index b823db3ae..1547bf4a1 100644 --- a/packages/hydrooj/src/ui.ts +++ b/packages/hydrooj/src/ui.ts @@ -51,7 +51,6 @@ if (useTerminal) { process.stdin.on('data', (data) => { // Control Seq? const seq = data.toString('hex'); - logger.info(data.toString('hex')); if (seq === '7f') input = input.substr(0, input.length - 1); else if (seq === '1b5b41') { // Arrow up diff --git a/packages/hydrooj/src/upgrade.ts b/packages/hydrooj/src/upgrade.ts index 2683d3f03..2c9fb53b1 100644 --- a/packages/hydrooj/src/upgrade.ts +++ b/packages/hydrooj/src/upgrade.ts @@ -25,15 +25,25 @@ async function iterateAllDomain(cb: (ddoc: DomainDoc, current?: number, total?: for (const i in ddocs) await cb(ddocs[i], +i, ddocs.length); } -async function iterateAllProblem(fields: problem.Pdoc.Field[], cb: (pdoc: problem.Pdoc, current?: number, total?: number) => Promise) { +interface PartialPdoc extends problem.Pdoc { + [key: string]: any, +} + +async function iterateAllProblem( + fields: (problem.Pdoc.Field | string)[], + cb: (pdoc: PartialPdoc, current?: number, total?: number) => Promise, +) { + if (!fields.includes('domainId')) fields.push('domainId'); + if (!fields.includes('docId')) fields.push('docId'); await iterateAllDomain(async (d) => { - const cursor = problem.getMulti(d._id, {}, fields); + const cursor = problem.getMulti(d._id, {}, fields as any); const total = await problem.getMulti(d._id, {}).count(); let i = 0; while (await cursor.hasNext()) { const doc = await cursor.next(); i++; - await cb(doc, i, total); + const res = await cb(doc, i, total); + if (res) await problem.edit(doc.domainId, doc.docId, res); } }); } @@ -162,7 +172,7 @@ const scripts: UpgradeScript[] = [ }, async function _8_9() { const _FRESH_INSTALL_IGNORE = 1; - await iterateAllProblem(['docId', 'domainId'], async (pdoc) => { + await iterateAllProblem([], async (pdoc) => { logger.info('%s/%s', pdoc.domainId, pdoc.docId); const [data, additional_file] = await Promise.all([ storage.list(`problem/${pdoc.domainId}/${pdoc.docId}/testdata/`), @@ -174,7 +184,21 @@ const scripts: UpgradeScript[] = [ for (let i = 0; i < additional_file.length; i++) { additional_file[i]._id = additional_file[i].name; } - await problem.edit(pdoc.domainId, pdoc.docId, { data, additional_file }); + return { data, additional_file }; + }); + return true; + }, + // Move category to tag + async function _9_10() { + const _FRESH_INSTALL_IGNORE = 1; + await iterateAllProblem(['tag', 'category'], async (pdoc) => { + await document.coll.updateOne( + { domainId: pdoc.domainId, docId: pdoc.docId }, + { + $set: { tag: [...pdoc.tag || [], ...pdoc.category || []] }, + $unset: { category: '' }, + }, + ); }); return true; }, diff --git a/packages/migrate-hustoj/script.ts b/packages/migrate-hustoj/script.ts index 81763cbe5..d9b6f8418 100644 --- a/packages/migrate-hustoj/script.ts +++ b/packages/migrate-hustoj/script.ts @@ -96,7 +96,7 @@ export async function run({ hint: pdoc.hint, source: pdoc.source, }, 'html'), - 1, pdoc.source.split(' '), undefined, pdoc.defunct === 'Y', + 1, pdoc.source.split(' '), pdoc.defunct === 'Y', ); const [cdoc] = await query(`SELECT * FROM 'privilege' WHERE rightstr = 'p${pdoc.problem_id}'`); await problem.edit('system', pid, { diff --git a/packages/syzoj-ng-api/handlers/problem.ts b/packages/syzoj-ng-api/handlers/problem.ts index c1ccc8a9c..f421a1694 100644 --- a/packages/syzoj-ng-api/handlers/problem.ts +++ b/packages/syzoj-ng-api/handlers/problem.ts @@ -30,14 +30,7 @@ export class QueryProblemSetAPIHandler extends Handler { } const query: FilterQuery = {}; if ($search) query.$text = { $search }; - if (tags?.length) { - query.$and = tags.map(($eq) => ({ - $or: [ - { category: { $elemMatch: { $eq } } }, - { tag: { $elemMatch: { $eq } } }, - ], - })); - } + if (tags?.length) query.$and = tags.map((tag) => ({ tag })); if (owner) query.owner = owner; if (!nonpublic) query.hidden = false; const [raw, count] = await Promise.all([