From a0734a1030ef8c542caec11c544f46eb04afc7b2 Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 1 Dec 2021 22:28:22 +0800 Subject: [PATCH] core: allow cross-domain problem search --- packages/hydrooj/bin/hydrooj.js | 1 + packages/hydrooj/package.json | 2 +- packages/hydrooj/src/handler/problem.ts | 38 +++++++-------- packages/hydrooj/src/interface.ts | 1 + packages/hydrooj/src/model/domain.ts | 5 ++ packages/hydrooj/src/model/problem.ts | 25 ++++++++++ packages/sonic/model.ts | 46 ++++++++++++++----- packages/sonic/package.json | 2 +- packages/sonic/script.ts | 14 ++++-- packages/ui-default/package.json | 2 +- .../templates/components/problem.html | 37 ++++++++------- 11 files changed, 120 insertions(+), 53 deletions(-) diff --git a/packages/hydrooj/bin/hydrooj.js b/packages/hydrooj/bin/hydrooj.js index 46e6c6e5b..0de652667 100755 --- a/packages/hydrooj/bin/hydrooj.js +++ b/packages/hydrooj/bin/hydrooj.js @@ -116,6 +116,7 @@ if (argv.args[0] === 'restore') { fs.copySync(`${dir}/env`, `${os.homedir()}/.hydro/env`, { overwrite: true }); } fs.removeSync(dir); + console.log('Successfully restored.'); return; } diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 56ba692ad..fe7cbb9f7 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "3.1.47", + "version": "3.2.0", "bin": "bin/hydrooj.js", "main": "src/loader", "module": "src/loader", diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index 6e5a1a1cd..d855a8607 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -128,17 +128,22 @@ export class ProblemMainHandler extends ProblemHandler { const query: FilterQuery = {}; let psdict = {}; const search = global.Hydro.lib.problemSearch; - let sort: number[]; - if (category.length) { - query.$and = []; - for (const tag of category) query.$and.push({ tag }); - } + let sort: string[]; + let fail = false; + if (category.length) query.$and = category.map((tag) => ({ tag })); if (q) category.push(q); if (category.length) this.extraTitleContent = category.join(','); if (q) { if (search) { const result = await search(domainId, q); - query.docId = { $in: result }; + if (!result.length) fail = true; + if (!query.$and) query.$and = []; + query.$and.push({ + $or: result.map((i) => { + const [did, docId] = i.split('/'); + return { domainId: did, docId: +docId }; + }), + }); sort = result; } else query.$text = { $search: q }; } @@ -147,26 +152,21 @@ export class ProblemMainHandler extends ProblemHandler { } await bus.serial('problem/list', query, this); // eslint-disable-next-line prefer-const - let [pdocs, ppcount, pcount] = await paginate( - problem.getMulti(domainId, query).sort({ sort: 1, docId: 1 }), - page, - system.get('pagination.problem'), - ); - if (sort) pdocs = pdocs.sort((a, b) => sort.indexOf(a.docId) - sort.indexOf(b.docId)); + let [pdocs, ppcount, pcount] = fail + ? [[], 0, 0] + : await problem.list( + domainId, query, undefined, + page, system.get('pagination.problem'), + ); + if (sort) pdocs = pdocs.sort((a, b) => sort.indexOf(`${a.domainId}/${a.docId}`) - sort.indexOf(`${b.domainId}/${b.docId}`)); if (q) { const pdoc = await problem.get(domainId, +q || q, problem.PROJECTION_LIST); - if (pdoc) { + if (pdoc && (!pdoc.hidden || this.user.own(pdoc) || this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN))) { const count = pdocs.length; pdocs = pdocs.filter((doc) => doc.docId !== pdoc.docId); pdocs.unshift(pdoc); pcount = pcount - count + pdocs.length; } - const canViewHidden = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN); - if (!canViewHidden) { - const unshift = []; - for (const doc of pdocs) if (doc.hidden && !this.user.own(doc)) unshift.push(doc); - for (const task of unshift) pdocs.unshift(task); - } } if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) { psdict = await problem.getListStatus( diff --git a/packages/hydrooj/src/interface.ts b/packages/hydrooj/src/interface.ts index 1fe3e1321..f7f240c42 100644 --- a/packages/hydrooj/src/interface.ts +++ b/packages/hydrooj/src/interface.ts @@ -367,6 +367,7 @@ export interface DomainDoc extends Record { export interface DomainUnion { _id: string; union: string[]; + problem: boolean; } // Message diff --git a/packages/hydrooj/src/model/domain.ts b/packages/hydrooj/src/model/domain.ts index 75cad97dc..1d74676a7 100644 --- a/packages/hydrooj/src/model/domain.ts +++ b/packages/hydrooj/src/model/domain.ts @@ -275,6 +275,11 @@ class DomainModel { static async getUnion(domainId: string) { return await collUnion.findOne({ _id: domainId }); } + + @ArgMethod + static async searchUnion(query) { + return await collUnion.find(query).toArray(); + } } bus.once('app/started', async () => { diff --git a/packages/hydrooj/src/model/problem.ts b/packages/hydrooj/src/model/problem.ts index 01e164fed..77161ba5c 100644 --- a/packages/hydrooj/src/model/problem.ts +++ b/packages/hydrooj/src/model/problem.ts @@ -16,6 +16,7 @@ import { import { buildProjection } from '../utils'; import { STATUS } from './builtin'; import * as document from './document'; +import DomainModel from './domain'; import storage from './storage'; export interface ProblemDoc extends Document { } @@ -132,6 +133,30 @@ export class ProblemModel { return document.getMulti(domainId, document.TYPE_PROBLEM, query, projection).sort({ sort: 1 }); } + static async list( + domainId: string, query: FilterQuery, + projection = ProblemModel.PROJECTION_LIST, page: number, pageSize: number, + ): Promise<[ProblemDoc[], number, number]> { + const union = await DomainModel.getUnion(domainId); + const domainIds = [domainId]; + if (union?.problem) domainIds.push(...union.union); + let count = 0; + const pdocs = []; + for (const id of domainIds) { + // TODO enhance performance + // eslint-disable-next-line no-await-in-loop + const ccount = await document.getMulti(id, document.TYPE_PROBLEM, query).count(); + if (pdocs.length < pageSize && (page - 1) * pageSize - count <= ccount) { + // eslint-disable-next-line no-await-in-loop + pdocs.push(...await document.getMulti(id, document.TYPE_PROBLEM, query, projection) + .sort({ sort: 1, docId: 1 }) + .skip(Math.max((page - 1) * pageSize - count, 0)).limit(pageSize - pdocs.length).toArray()); + } + count += ccount; + } + return [pdocs, Math.ceil(count / pageSize), count]; + } + static getStatus(domainId: string, docId: number, uid: number) { return document.getStatus(domainId, document.TYPE_PROBLEM, docId, uid); } diff --git a/packages/sonic/model.ts b/packages/sonic/model.ts index a0b74551d..a8b60ef87 100644 --- a/packages/sonic/model.ts +++ b/packages/sonic/model.ts @@ -1,26 +1,50 @@ +import DomainModel from 'hydrooj/src/model/domain'; import * as system from 'hydrooj/src/model/system'; import * as bus from 'hydrooj/src/service/bus'; import sonic from './service'; bus.on('problem/add', async (doc, docId) => { - await sonic.push('problem', `${doc.domainId}@title`, docId.toString(), doc.title); - await sonic.push('problem', `${doc.domainId}@content`, docId.toString(), doc.content.toString()); + const union = await DomainModel.searchUnion({ union: doc.domainId, problem: true }); + const tasks = []; + for (const domainId of [doc.domainId, ...union.map((i) => i._id)]) { + tasks.push( + sonic.push('problem', `${domainId}@title`, `${doc.domainId}/${docId}`, doc.title), + sonic.push('problem', `${domainId}@content`, `${doc.domainId}/${docId}`, doc.content.toString()), + ); + } + await Promise.all(tasks); }); bus.on('problem/edit', async (pdoc) => { - await sonic.flusho('problem', `${pdoc.domainId}@title`, pdoc.docId.toString()); - await sonic.flusho('problem', `${pdoc.domainId}@content`, pdoc.docId.toString()); - await sonic.push('problem', `${pdoc.domainId}@title`, pdoc.docId.toString(), pdoc.title); - await sonic.push('problem', `${pdoc.domainId}@content`, pdoc.docId.toString(), pdoc.content.toString()); + const union = await DomainModel.searchUnion({ union: pdoc.domainId, problem: true }); + const tasks = []; + const id = `${pdoc.domainId}/${pdoc.docId}`; + for (const domainId of [pdoc.domainId, ...union.map((i) => i._id)]) { + tasks.push( + sonic.flusho('problem', `${domainId}@title`, id) + .then(() => sonic.push('problem', `${domainId}@title`, id, pdoc.title)), + sonic.flusho('problem', `${domainId}@content`, id) + .then(() => sonic.push('problem', `${domainId}@content`, id, pdoc.content.toString())), + ); + } + await Promise.all(tasks); }); bus.on('problem/del', async (domainId, docId) => { - await sonic.flusho('problem', `${domainId}@title`, docId.toString()); - await sonic.flusho('problem', `${domainId}@content`, docId.toString()); + const union = await DomainModel.searchUnion({ union: domainId, problem: true }); + const tasks = []; + const id = `${domainId}/${docId}`; + for (const domain of [domainId, ...union.map((i) => i._id)]) { + tasks.push( + sonic.flusho('problem', `${domain}@title`, id), + sonic.flusho('problem', `${domain}@content`, id), + ); + } + await Promise.all(tasks); }); global.Hydro.lib.problemSearch = async (domainId: string, query: string, limit = system.get('pagination.problem')) => { - const docIds = await sonic.query('problem', `${domainId}@title`, query, { limit }); - if (limit - docIds.length > 0) docIds.push(...await sonic.query('problem', `${domainId}@content`, query, { limit: limit - docIds.length })); - return docIds.map(Number); + const ids = await sonic.query('problem', `${domainId}@title`, query, { limit }); + if (limit - ids.length > 0) ids.push(...await sonic.query('problem', `${domainId}@content`, query, { limit: limit - ids.length })); + return ids; }; diff --git a/packages/sonic/package.json b/packages/sonic/package.json index a39cd340a..79a54da56 100644 --- a/packages/sonic/package.json +++ b/packages/sonic/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/sonic", - "version": "1.1.1", + "version": "1.2.0", "description": "Sonic search service", "main": "service.js", "typings": "service.d.ts", diff --git a/packages/sonic/script.ts b/packages/sonic/script.ts index 60cc666a1..44160e806 100644 --- a/packages/sonic/script.ts +++ b/packages/sonic/script.ts @@ -1,3 +1,4 @@ +import DomainModel from 'hydrooj/src/model/domain'; import { iterateAllProblem, iterateAllProblemInDomain } from 'hydrooj/src/pipelineUtils'; import sonic from './service'; @@ -10,10 +11,15 @@ export async function run({ domainId }, report) { const cb = async (pdoc) => { i++; if (!(i % 1000)) report({ message: `${i} problems indexed` }); - await Promise.all([ - sonic.push('problem', `${pdoc.domainId}@title`, pdoc.docId.toString(), pdoc.title), - sonic.push('problem', `${pdoc.domainId}@content`, pdoc.docId.toString(), pdoc.content.toString()), - ]).catch((e) => console.log(`${pdoc.domainId}/${pdoc.docId}`, e)); + const union = await DomainModel.searchUnion({ union: pdoc.domainId, problem: true }); + const tasks = []; + for (const did of [domainId, ...union.map((j) => j._id)]) { + tasks.push( + pdoc.title && sonic.push('problem', `${did}@title`, `${pdoc.domainId}/${pdoc.docId}`, pdoc.title), + pdoc.content.toString() && sonic.push('problem', `${did}@content`, `${pdoc.domainId}/${pdoc.docId}`, pdoc.content.toString()), + ); + } + await Promise.all(tasks).catch((e) => console.log(`${pdoc.domainId}/${pdoc.docId}`, e)); }; if (domainId) await iterateAllProblemInDomain(domainId, ['title', 'content'], cb); else await iterateAllProblem(['title', 'content'], cb); diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 30d35d82e..4abe1500e 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.26.26", + "version": "4.27.0", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/templates/components/problem.html b/packages/ui-default/templates/components/problem.html index 5df186850..681971de7 100644 --- a/packages/ui-default/templates/components/problem.html +++ b/packages/ui-default/templates/components/problem.html @@ -1,23 +1,28 @@ {% macro render_problem_title(pdoc, tdoc=none, show_tags=true, show_invisible_flag=true, invalid=false, inline=false) %} -{%- if not invalid %} - -{%- endif %} -{{ pdoc.pid if pdoc.pid else 'P'+pdoc.docId }}. {{ pdoc.title }} -{%- if not invalid %} +{%- if not invalid -%} + {% set _linkArgs = { pid:pdoc.pid|default(pdoc.docId) } %} + {% if tdoc %}{{ set(_linkArgs, 'query', {tid:tdoc.docId}) }}{% endif %} + {% if pdoc.domainId !== handler.domainId %}{{ set(_linkArgs, 'domainId', pdoc.domainId) }}{% endif %} + +{%- endif -%} +{%- if pdoc.domainId !== handler.domainId -%} + {{ pdoc.domainId }}# +{%- endif -%} +{%- if pdoc.pid -%} + {{ pdoc.pid }} +{%- else -%} + {{ 'P'+pdoc.docId if pdoc.domainId === handler.domainId else pdoc.docId }} +{%- endif -%} +. {{ pdoc.title }} +{%- if not invalid -%} -{%- endif %} -{%- if pdoc.hidden and show_invisible_flag %} +{%- endif -%} +{%- if pdoc.hidden and show_invisible_flag -%} ({{ _('Hidden') }}) -{%- endif %} -{%- if show_tags %} +{%- endif -%} +{%- if show_tags -%} {{ render_problem_tags(pdoc, inline=inline) }} -{%- endif %} +{%- endif -%} {% endmacro %} {% macro render_problem_tags(pdoc, show_none_label=false, inline=false) %}