core: 整合tag与category

This commit is contained in:
undefined committed 2021-03-07 14:25:16 +08:00
1 parent 454afbcb08
commit 7a41fecb0e
15 files changed
+116 -117

No files matched your search

+1 -1
View File
@@ -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++) {
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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",
+2 -4
View File
@@ -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;
+32 -78
View File
@@ -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<Pdoc> = 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<Pdoc> = { 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<Pdoc> = { 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<Pdoc> = {
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);
+1 -1
View File
@@ -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 });
+2 -1
View File
@@ -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,
+36 -6
View File
@@ -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<string, Logger> = {};
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<LogType, LogFunction> { }
export class Logger {
static readonly SUCCESS = 1;
static readonly ERROR = 1;
@@ -16,10 +24,24 @@ export class Logger {
static showDiff = false;
static levels: Record<string, number> = {};
static lastTime = 0;
static formatters: Record<string, (this: Logger, value: any) => 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, (this: Logger, value: any) => 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);
-2
View File
@@ -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
+3 -4
View File
@@ -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<Pdoc> = {
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);
+5 -2
View File
@@ -52,9 +52,12 @@ export interface EventMap {
'domain/update': (domainId: string, $set: Partial<DomainDoc>, ddoc: DomainDoc) => VoidReturn
'document/add': (doc: any) => VoidReturn
'document/set': <T extends keyof DocType>
(domainId: string, docType: T, docId: DocType[T], $set: any, $unset: OnlyFieldsOfType<DocType[T], any, true | '' | 1>) => VoidReturn
'document/set': <T extends keyof DocType>(
domainId: string, docType: T, docId: DocType[T],
$set: any, $unset: OnlyFieldsOfType<DocType[T], any, true | '' | 1>
) => VoidReturn
'problem/before-edit': (doc: Partial<Pdoc>) => VoidReturn
'problem/edit': (doc: Pdoc) => VoidReturn
'problem/list': (query: FilterQuery<Pdoc>, handler: any) => VoidReturn
'problem/setting': (update: Partial<Pdoc>, handler: any) => VoidReturn
-1
View File
@@ -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
+29 -5
View File
@@ -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<any>) {
interface PartialPdoc extends problem.Pdoc {
[key: string]: any,
}
async function iterateAllProblem(
fields: (problem.Pdoc.Field | string)[],
cb: (pdoc: PartialPdoc, current?: number, total?: number) => Promise<any>,
) {
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;
},
+1 -1
View File
@@ -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, {
+1 -8
View File
@@ -30,14 +30,7 @@ export class QueryProblemSetAPIHandler extends Handler {
}
const query: FilterQuery<Pdoc> = {};
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([