From fff2172c3864b3e1553a756f279dfa82af039b95 Mon Sep 17 00:00:00 2001 From: masnn Date: Fri, 29 May 2020 13:05:52 +0800 Subject: [PATCH] =?UTF-8?q?!UNSTABLE=20=E4=BC=98=E5=8C=96=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E7=BB=93=E6=9E=84=EF=BC=8C=E4=B8=BAdomain?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=87=86=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 1 - hydro/handler/contest.js | 99 ++-- hydro/handler/discussion.js | 91 ++-- hydro/handler/home.js | 28 +- hydro/handler/import.js | 10 +- hydro/handler/judge.js | 18 +- hydro/handler/manage.js | 26 +- hydro/handler/problem.js | 155 +++--- hydro/handler/record.js | 38 +- hydro/handler/training.js | 30 +- hydro/handler/user.js | 18 +- hydro/interface.js | 1 + hydro/loader.js | 10 +- hydro/model/blacklist.js | 4 +- hydro/model/builtin.js | 10 +- hydro/model/contest.js | 95 ++-- hydro/model/discussion.js | 167 +++---- hydro/model/document.js | 366 +++++++++++++++ hydro/model/domain.js | 96 ++++ hydro/model/index.js | 1 + hydro/model/message.js | 4 +- hydro/model/problem.js | 162 +++---- hydro/model/record.js | 67 +-- hydro/model/solution.js | 105 ++--- hydro/model/training.js | 114 +++-- hydro/model/user.js | 119 +++-- hydro/permission.js | 3 + hydro/script/install.js | 45 +- hydro/service/server.js | 22 +- module/migrate-vijos/script.js | 444 ++++++++---------- templates/components/problem.html | 2 +- templates/contest_main.html | 8 +- templates/contest_scoreboard.html | 6 +- templates/main.html | 12 +- templates/partials/contest_sidebar.html | 16 +- .../partials/problem_sidebar_contest.html | 4 +- .../partials/problem_sidebar_normal.html | 14 +- templates/record_detail.html | 2 +- templates/training_detail.html | 2 +- templates/training_main.html | 8 +- 40 files changed, 1411 insertions(+), 1012 deletions(-) create mode 100644 hydro/model/document.js create mode 100644 hydro/model/domain.js diff --git a/.eslintrc.js b/.eslintrc.js index 18980dfb8..154bb052b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,7 +16,6 @@ module.exports = { rules: { "indent": ["warn", 4], "no-plusplus": "off", - "no-underscore-dangle": "off", "no-console": "off", "no-extend-native": "off", "no-restricted-syntax": "off", diff --git a/hydro/handler/contest.js b/hydro/handler/contest.js index 46c1979d0..81f32309a 100644 --- a/hydro/handler/contest.js +++ b/hydro/handler/contest.js @@ -14,24 +14,24 @@ const { Route, Handler } = require('../service/server'); const ContestHandler = contest.ContestHandlerMixin(Handler); class ContestListHandler extends ContestHandler { - async get({ rule = 0, page = 1 }) { + async get({ domainId, rule = 0, page = 1 }) { this.response.template = 'contest_main.html'; let tdocs; let qs; let tpcount; if (!rule) { - tdocs = contest.getMulti().sort({ beginAt: -1 }); + tdocs = contest.getMulti(domainId).sort({ beginAt: -1 }); qs = ''; } else { if (!contest.CONTEST_RULES.includes(rule)) throw new ValidationError('rule'); - tdocs = contest.getMulti({ rule }).sort({ beginAt: -1 }); + tdocs = contest.getMulti(domainId, { rule }).sort({ beginAt: -1 }); qs = 'rule={0}'.format(rule); } // eslint-disable-next-line prefer-const [tdocs, tpcount] = await paginate(tdocs, page, await system.get('CONTEST_PER_PAGE')); const tids = []; - for (const tdoc of tdocs) tids.push(tdoc._id); - const tsdict = await contest.getListStatus(this.user._id, tids); + for (const tdoc of tdocs) tids.push(tdoc.docId); + const tsdict = await contest.getListStatus(domainId, this.user._id, tids); const path = [ ['Hydro', '/'], ['contest_main', null], @@ -47,11 +47,11 @@ class ContestDetailHandler extends ContestHandler { this.tdoc = await contest.get(tid); } - async get({ page = 1 }) { + async get({ domainId, page = 1 }) { this.response.template = 'contest_detail.html'; const [tsdoc, pdict] = await Promise.all([ - contest.getStatus(this.tdoc._id, this.user._id), - problem.getList(this.tdoc.pids), + contest.getStatus(domainId, this.tdoc.docId, this.user._id), + problem.getList(domainId, this.tdoc.pids), ]); const psdict = {}; let rdict = {}; let attended; @@ -61,7 +61,7 @@ class ContestDetailHandler extends ContestHandler { if (this.canShowRecord(this.tdoc)) { const q = []; for (const i in psdict) q.push(psdict[i].rid); - rdict = await record.getList(q); + rdict = await record.getList(domainId, q); } else { for (const i in psdict) rdict[psdict[i].rid] = { _id: psdict[i].rid }; } @@ -77,16 +77,16 @@ class ContestDetailHandler extends ContestHandler { }; } - async postAttend() { - if (contest.isDone(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id); - await contest.attend(this.tdoc._id, this.user._id); + async postAttend({ domainId }) { + if (contest.isDone(this.tdoc)) throw new ContestNotLiveError(this.tdoc.docId); + await contest.attend(domainId, this.tdoc.docId, this.user._id); this.back(); } } class ContestScoreboardHandler extends ContestDetailHandler { - async get({ tid }) { - const [tdoc, rows, udict] = await this.getScoreboard(tid); + async get({ domainId, tid }) { + const [tdoc, rows, udict] = await this.getScoreboard(domainId, tid); const path = [ ['Hydro', '/'], ['contest_main', '/c'], @@ -101,32 +101,33 @@ class ContestScoreboardHandler extends ContestDetailHandler { } class ContestScoreboardDownloadHandler extends ContestDetailHandler { - async get({ tid, ext }) { + async get({ domainId, tid, ext }) { const getContent = { csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`, html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }), }; if (!getContent[ext]) throw new ValidationError('ext'); - const [, rows] = await this.getScoreboard(tid, true); + const [, rows] = await this.getScoreboard(domainId, tid, true); this.binary(await getContent[ext](rows), `${this.tdoc.title}.${ext}`); } } class ContestEditHandler extends ContestDetailHandler { - async prepare({ tid }) { - const tdoc = await contest.get(tid); - if (!tdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_CONTEST); + async prepare() { + if (this.tdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_CONTEST); } async get() { this.response.template = 'contest_edit.html'; const rules = {}; - for (const i in contest.RULES) { rules[i] = contest.RULES[i].TEXT; } + for (const i in contest.RULES) { + rules[i] = contest.RULES[i].TEXT; + } const duration = (this.tdoc.endAt.getTime() - this.tdoc.beginAt.getTime()) / 3600 / 1000; const path = [ ['Hydro', '/'], ['contest_main', '/c'], - [this.tdoc.title, `/c/${this.tdoc._id}`, true], + [this.tdoc.title, `/c/${this.tdoc.docId}`, true], ['contest_edit', null], ]; const dt = this.tdoc.beginAt; @@ -143,7 +144,7 @@ class ContestEditHandler extends ContestDetailHandler { } async post({ - beginAtDate, beginAtTime, duration, title, content, rule, pids, + domainId, beginAtDate, beginAtTime, duration, title, content, rule, pids, }) { let beginAt; try { @@ -153,36 +154,35 @@ class ContestEditHandler extends ContestDetailHandler { } const endAt = new Date(beginAt + duration * 3600 * 1000); if (beginAt >= endAt) throw new ValidationError('duration'); - pids = await this.verifyProblems(pids); - await contest.edit(this.tdoc._id, title, content, rule, beginAt, endAt, pids); + pids = await this.verifyProblems(domainId, pids); + await contest.edit(domainId, this.tdoc.docId, title, content, rule, beginAt, endAt, pids); if (this.tdoc.beginAt !== beginAt || this.tdoc.endAt !== endAt || Array.isDiff(this.tdoc.pids, pids) || this.tdoc.rule !== rule) { - await contest.recalcStatus(this.tdoc._id); + await contest.recalcStatus(this.tdoc.docId); } - if (this.preferJson) this.response.body = { tid: this.tdoc._id }; - else this.response.redirect = `/c/${this.tdoc._id}`; + if (this.preferJson) this.response.body = { tid: this.tdoc.docId }; + else this.response.redirect = `/c/${this.tdoc.docId}`; } } class ContestProblemHandler extends ContestDetailHandler { - async prepare({ tid, pid }) { + async prepare({ domainId, tid, pid }) { [this.tdoc, this.pdoc] = await Promise.all([ - contest.get(tid), - problem.get(pid, this.user._id), + contest.get(domainId, tid), + problem.get(domainId, pid, this.user._id), ]); [this.tsdoc, this.udoc] = await Promise.all([ - contest.getStatus(this.tdoc._id, this.user._id), - user.getById(this.tdoc.owner), + contest.getStatus(domainId, this.tdoc.docId, this.user._id), + user.getById(domainId, this.tdoc.owner), ]); this.attended = this.tsdoc && this.tsdoc.attend === 1; this.response.template = 'problem_detail.html'; if (contest.isDone(this.tdoc)) { - if (!this.attended) throw new ContestNotAttendedError(this.tdoc._id); - if (!contest.isOngoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id); + if (!this.attended) throw new ContestNotAttendedError(this.tdoc.docId); + if (!contest.isOngoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc.docId); } - console.log(this.tdoc.pids, this.pdoc._id); - if (!this.tdoc.pids.map((s) => s.toString()).includes(this.pdoc._id.toString())) { - throw new ProblemNotFoundError(pid, this.tdoc._id); + if (!this.tdoc.pids.map((s) => s.toString()).includes(this.pdoc.docId.toString())) { + throw new ProblemNotFoundError(pid, this.tdoc.docId); } } @@ -205,10 +205,10 @@ class ContestProblemHandler extends ContestDetailHandler { } class ContestProblemSubmitHandler extends ContestProblemHandler { - async get({ tid, pid }) { + async get({ domainId, tid, pid }) { let rdocs = []; if (this.canShowRecord(this.tdoc)) { - rdocs = await record.getUserInProblemMulti(this.user._id, this.pdoc._id, true) + rdocs = await record.getUserInProblemMulti(domainId, this.user._id, this.pdoc.docId, true) .sort({ _id: -1 }).limit(10).toArray(); } this.response.template = 'problem_submit.html'; @@ -230,12 +230,19 @@ class ContestProblemSubmitHandler extends ContestProblemHandler { }; } - async post({ tid, lang, code }) { + async post({ + domainId, tid, lang, code, + }) { await this.limitRate('add_record', 60, 100); - const rid = await record.add({ - pid: this.pdoc._id, uid: this.user._id, lang, code, tid: this.tdoc._id, hidden: true, + const rid = await record.add(domainId, { + pid: this.pdoc.docId, + uid: this.user._id, + tid: this.tdoc.docId, + lang, + code, + hidden: true, }); - await contest.updateStatus(this.tdoc._id, this.user._id, rid, this.pdoc._id, false, 0); + await contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId, false, 0); if (!this.canShowRecord(this.tdoc)) { this.response.body = { tid }; this.response.redirect = `/c/${tid}`; @@ -275,7 +282,7 @@ class ContestCreateHandler extends ContestHandler { } async post({ - title, content, rule, beginAtDate, beginAtTime, duration, pids, + domainId, title, content, rule, beginAtDate, beginAtTime, duration, pids, }) { let beginAt; try { @@ -285,8 +292,8 @@ class ContestCreateHandler extends ContestHandler { } const endAt = new Date(beginAt.getTime() + duration * 3600 * 1000); if (beginAt >= endAt) throw new ValidationError('duration'); - pids = await this.verifyProblems(pids); - const tid = await contest.add(title, content, this.user._id, rule, beginAt, endAt, pids); + pids = await this.verifyProblems(domainId, pids); + const tid = await contest.add(domainId, title, content, this.user._id, rule, beginAt, endAt, pids); this.response.body = { tid }; this.response.redirect = `/c/${tid}`; } diff --git a/hydro/handler/discussion.js b/hydro/handler/discussion.js index 59641bf5e..f46730cd0 100644 --- a/hydro/handler/discussion.js +++ b/hydro/handler/discussion.js @@ -2,12 +2,8 @@ const paginate = require('../lib/paginate'); const system = require('../model/system'); const user = require('../model/user'); const discussion = require('../model/discussion'); -const { - Route, Handler, -} = require('../service/server'); -const { - DiscussionNotFoundError, DocumentNotFoundError, -} = require('../error'); +const { Route, Handler } = require('../service/server'); +const { DiscussionNotFoundError, DocumentNotFoundError } = require('../error'); const { PERM_VIEW_DISCUSSION, PERM_EDIT_DISCUSSION, PERM_EDIT_DISCUSSION_REPLY, PERM_DELETE_DISCUSSION, PERM_DELETE_DISCUSSION_REPLY, PERM_HIGHLIGHT_DISCUSSION, @@ -16,21 +12,21 @@ const { class DiscussionHandler extends Handler { async _prepare({ - type = 'node', docId, name, did, drid, drrid, + domainId, type = 'node', docId, name, did, drid, drrid, }) { this.checkPerm(PERM_VIEW_DISCUSSION); docId = docId || name; if (did) { - this.ddoc = await discussion.get(did); + this.ddoc = await discussion.get(domainId, did); if (!this.ddoc) throw new DiscussionNotFoundError(did); type = this.ddoc.parentType; docId = this.ddoc.parentId; if (drid) { - this.drdoc = await discussion.getReply(drid, did); + this.drdoc = await discussion.getReply(domainId, drid, did); if (!this.drdoc) throw new DiscussionNotFoundError(drid); if (this.drdoc.parent !== this.ddoc._id) throw new DocumentNotFoundError(drid); if (drrid) { - [, this.drrdoc] = await discussion.getTailReply(drid, drrid); + [, this.drrdoc] = await discussion.getTailReply(domainId, drid, drrid); if (!this.drrdoc) throw new DiscussionNotFoundError(drrid); if (this.drrdoc.parent !== this.drdoc._id) { throw new DocumentNotFoundError(drid); @@ -41,23 +37,22 @@ class DiscussionHandler extends Handler { // TODO(twd2): do more visibility check eg. contest // TODO(twd2): exclude problem/contest discussions? // TODO(iceboy): continuation based pagination. - this.vnode = await discussion.getVnode({ parentType: type, parentId: docId }, this); + this.vnode = await discussion.getVnode(domainId, { parentType: type, parentId: docId }, this); if (this.ddoc) { this.ddoc.parentType = this.ddoc.parentType || this.vnode.type; this.ddoc.parentId = this.ddoc.parentId || this.vnode.id; } - console.log(this.vnode); } } class DiscussionMainHandler extends DiscussionHandler { - async get({ page = 1 }) { + async get({ domainId, page = 1 }) { const [ddocs, dpcount] = await paginate( - discussion.getMulti(), + discussion.getMulti(domainId), page, await system.get('DISCUSSION_PER_PAGE'), ); - const udict = await user.getList(ddocs.map((ddoc) => ddoc.owner)); + const udict = await user.getList(domainId, ddocs.map((ddoc) => ddoc.owner)); const path = [ ['Hydro', '/'], ['discussion_main', null], @@ -71,15 +66,15 @@ class DiscussionMainHandler extends DiscussionHandler { class DiscussionNodeHandler extends DiscussionHandler { async get({ - type = 'node', docId, name, page = 1, + domainId, type = 'node', docId, name, page = 1, }) { docId = docId || name; const [ddocs, dpcount] = await paginate( - discussion.getMulti({ type, docId }), + discussion.getMulti(domainId, { parentType: type, parentId: docId }), page, await system.get('DISCUSSION_PER_PAGE'), ); - const udict = await user.getList(ddocs.map((ddoc) => ddoc.owner)); + const udict = await user.getList(domainId, ddocs.map((ddoc) => ddoc.owner)); const path = [ ['Hydro', '/'], ['discussion_main', '/discuss'], @@ -117,13 +112,13 @@ class DiscussionCreateHandler extends DiscussionHandler { } async post({ - type = 'node', docId, name, title, content, highlight, + domainId, type = 'node', docId, name, title, content, highlight, }) { docId = docId || name; this.limitRate('add_discussion', 3600, 30); if (highlight) this.checkPerm(PERM_HIGHLIGHT_DISCUSSION); const did = await discussion.add( - type, docId, this.user._id, + domainId, type, docId, this.user._id, title, content, this.request.ip, highlight, ); this.response.body = { did }; @@ -132,16 +127,16 @@ class DiscussionCreateHandler extends DiscussionHandler { } class DiscussionDetailHandler extends DiscussionHandler { - async get({ did, page = 1 }) { + async get({ domainId, did, page = 1 }) { const dsdoc = this.user.hasPerm(PERM_LOGGEDIN) - ? await discussion.getStatus(did, this.user._id) + ? await discussion.getStatus(domainId, did, this.user._id) : null; const [drdocs, pcount, drcount] = await paginate( - discussion.getMultiReply(did), + discussion.getMultiReply(domainId, did), page, await system.get('REPLY_PER_PAGE'), ); - const uids = drdocs.map((drdoc) => drdoc.owner); + const uids = drdocs.map(domainId, (drdoc) => drdoc.owner); uids.push(this.ddoc.owner); for (const drdoc of drdocs) { if (drdoc.reply) { @@ -150,7 +145,7 @@ class DiscussionDetailHandler extends DiscussionHandler { } } } - const udict = await user.getList(uids); + const udict = await user.getList(domainId, uids); const path = [ ['Hydro', '/'], ['discussion_main', '/discuss'], @@ -163,54 +158,56 @@ class DiscussionDetailHandler extends DiscussionHandler { }; } - async postReply({ did, content }) { + async postReply({ domainId, did, content }) { this.checkPerm(PERM_LOGGEDIN); this.checkPerm(PERM_REPLY_DISCUSSION); this.limitRate('add_discussion', 3600, 30); - await discussion.addReply(did, this.user._id, content, this.request.ip); + await discussion.addReply(domainId, did, this.user._id, content, this.request.ip); this.back(); } - async postTailReply({ drid, content }) { + async postTailReply({ domainId, drid, content }) { this.checkPerm(PERM_LOGGEDIN); this.checkPerm(PERM_REPLY_DISCUSSION); this.limitRate('add_discussion', 3600, 30); - await discussion.addTailReply(drid, this.user._id, content, this.request.ip); + await discussion.addTailReply(domainId, drid, this.user._id, content, this.request.ip); this.back(); } - async postEditReply({ drid, content }) { + async postEditReply({ domainId, drid, content }) { if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_DISCUSSION_REPLY); - await discussion.editReply(drid, content); + await discussion.editReply(domainId, drid, content); this.back(); } - async postDeleteReply({ drid }) { + async postDeleteReply({ domainId, drid }) { if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM_DELETE_DISCUSSION_REPLY); - await discussion.deleteReply(drid); + await discussion.deleteReply(domainId, drid); this.back(); } - async postEditTailReply({ drid, drrid, content }) { + async postEditTailReply({ + domainId, drid, drrid, content, + }) { if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_DISCUSSION_REPLY); - await discussion.editTailReply(drid, drrid, content); + await discussion.editTailReply(domainId, drid, drrid, content); this.back(); } - async postDeleteTailReply({ drid, drrid }) { + async postDeleteTailReply({ domainId, drid, drrid }) { if (this.drrdoc.owner !== this.user._id) this.checkPerm(PERM_DELETE_DISCUSSION_REPLY); - await discussion.deleteTailReply(drid, drrid); + await discussion.deleteTailReply(domainId, drid, drrid); this.back(); } - async postStar({ did, star }) { - await discussion.setStar(did, this.user._id, true); + async postStar({ domainId, did, star }) { + await discussion.setStar(domainId, did, this.user._id, true); this.response.body = { star }; this.response.direct = this.request.path; } - async postUnstar({ did, star }) { - await discussion.setStar(did, this.user._id, false); + async postUnstar({ domainId, did, star }) { + await discussion.setStar(domainId, did, this.user._id, false); this.response.body = { star }; this.response.direct = this.request.path; } @@ -245,7 +242,7 @@ class DiscussionEditHandler extends DiscussionHandler { ['Hydro', '/'], ['discussion_main', '/discuss'], [this.vnode.title, `/discuss/${this.ddoc.parentType}`, true], - [this.ddoc.title, `/discuss/${this.ddoc._id}`, true], + [this.ddoc.title, `/discuss/${this.ddoc.docId}`, true], ['discussion_edit', null], ]; this.response.template = 'discussion_edit.html'; @@ -253,20 +250,20 @@ class DiscussionEditHandler extends DiscussionHandler { } async post({ - did, title, content, highlight, operation, + domainId, did, title, content, highlight, operation, }) { if (operation) return; if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_DISCUSSION); if (highlight && !this.ddoc.highlight) this.checkPerm(PERM_HIGHLIGHT_DISCUSSION); - await discussion.edit(did, title, content, highlight); + await discussion.edit(domainId, did, title, content, highlight); this.response.body = { did }; this.response.redirect = `/discuss/${did}`; } - async postDelete({ did }) { + async postDelete({ domainId, did }) { if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM_DELETE_DISCUSSION); - await discussion.delete(did); - this.response.body = { type: this.ddoc.type, parent: this.ddoc.parent }; + await discussion.delete(domainId, did); + this.response.body = { type: this.ddoc.parentType, parent: this.ddoc.parentId }; this.response.redirect = `/discuss/${this.ddoc.type}/${this.ddoc.parent}`; } } diff --git a/hydro/handler/home.js b/hydro/handler/home.js index 4ce253bbc..9a0a16441 100644 --- a/hydro/handler/home.js +++ b/hydro/handler/home.js @@ -22,49 +22,49 @@ const { } = require('../permission'); class HomeHandler extends Handler { - async contest() { + async contest(domainId) { if (this.user.hasPerm(PERM_VIEW_CONTEST)) { - const tdocs = await contest.getMulti() + const tdocs = await contest.getMulti(domainId) .limit(await system.get('CONTESTS_ON_MAIN')) .toArray(); const tsdict = await contest.getListStatus( - this.user._id, tdocs.map((tdoc) => tdoc._id), + domainId, this.user._id, tdocs.map((tdoc) => tdoc.docId), ); return [tdocs, tsdict]; } return [[], {}]; } - async training() { + async training(domainId) { if (this.user.hasPerm(PERM_VIEW_TRAINING)) { - const tdocs = await training.getMulti() + const tdocs = await training.getMulti(domainId) .sort('_id', 1) .limit(await system.get('TRAININGS_ON_MAIN')) .toArray(); const tsdict = await training.getListStatus( - this.user._id, tdocs.map((tdoc) => tdoc._id), + domainId, this.user._id, tdocs.map((tdoc) => tdoc.docId), ); return [tdocs, tsdict]; } return [[], {}]; } - async discussion() { + async discussion(domainId) { if (this.user.hasPerm(PERM_VIEW_DISCUSSION)) { - const ddocs = await discussion.getMulti() + const ddocs = await discussion.getMulti(domainId) .limit(await system.get('DISCUSSIONS_ON_MAIN')) .toArray(); - const vndict = await discussion.getListVnodes(ddocs, this); + const vndict = await discussion.getListVnodes(domainId, ddocs, this); return [ddocs, vndict]; } return [[], {}]; } - async get() { + async get({ domainId }) { const [[tdocs, tsdict], [trdocs, trsdict], [ddocs, vndict]] = await Promise.all([ - this.contest(), this.training(), this.discussion(), + this.contest(domainId), this.training(domainId), this.discussion(domainId), ]); - const udict = await user.getList(ddocs.map((ddoc) => ddoc.owner)); + const udict = await user.getList(domainId, ddocs.map((ddoc) => ddoc.owner)); this.response.template = 'main.html'; this.response.body = { tdocs, tsdict, trdocs, trsdict, ddocs, vndict, udict, @@ -153,7 +153,7 @@ class HomeSettingsHandler extends Handler { async post(args) { // FIXME validation - await user.setById(this.user._id, args); + await user.setById(domainId, this.user._id, args); this.back(); } } @@ -206,7 +206,7 @@ class HomeMessagesHandler extends Handler { } async postSend({ uid, content, type = 'full' }) { - const udoc = await user.getById(uid); + const udoc = await user.getById('system', uid); let mdoc = await message.send(this.user._id, uid, content); if (type === 'single') { mdoc = mdoc.reply[mdoc.reply.length - 1]; diff --git a/hydro/handler/import.js b/hydro/handler/import.js index 8717fc709..37b7e0ab2 100644 --- a/hydro/handler/import.js +++ b/hydro/handler/import.js @@ -20,7 +20,7 @@ class ProblemImportHandler extends Handler { } async post({ - url, pid, hidden, remoteType, + domainId, url, pid, hidden, remoteType, }) { if (typeof global.Hydro.lib[`import.${remoteType}`] !== 'function') { throw new ValidationError('remoteType'); @@ -28,10 +28,10 @@ class ProblemImportHandler extends Handler { const [pdoc, testdata] = await global.Hydro.lib[`import.${remoteType}`](url, this); if (pid) pdoc.pid = pid; if (hidden) pdoc.hidden = true; - const _id = await problem.add(pdoc); - await problem.setTestdata(_id, testdata); - this.response.body = { pid: pid || _id }; - this.response.redirect = `/p/${pid || _id}/settings`; + const docId = await problem.add(domainId, pdoc.title, pdoc.content, this.user._id, pdoc); + await problem.setTestdata(docId, testdata); + this.response.body = { pid: pid || docId }; + this.response.redirect = `/p/${pid || docId}/settings`; } } diff --git a/hydro/handler/judge.js b/hydro/handler/judge.js index 971a13c76..8f2ebf59f 100644 --- a/hydro/handler/judge.js +++ b/hydro/handler/judge.js @@ -16,14 +16,14 @@ async function _postJudge(rdoc) { const tasks = []; if (rdoc.tid) { tasks.push( - contest.updateStatus(rdoc.tid, rdoc.uid, rdoc._id, rdoc.pid, accept, rdoc.score), + contest.updateStatus(rdoc.domainId, rdoc.tid, rdoc.uid, rdoc._id, rdoc.pid, accept, rdoc.score), ); } - if (await problem.updateStatus(rdoc.pid, rdoc.uid, rdoc._id, rdoc.status)) { + if (await problem.updateStatus(rdoc.rdoc.pid, rdoc.uid, rdoc._id, rdoc.status)) { if (accept && !rdoc.rejudged) { tasks.push( - problem.inc(rdoc.pid, 'nAccept', 1), - user.inc(rdoc.uid, 'nAccept', 1), + problem.inc(rdoc.domainId, rdoc.pid, 'nAccept', 1), + user.inc(rdoc.domainId, rdoc.uid, 'nAccept', 1), ); } } @@ -31,7 +31,7 @@ async function _postJudge(rdoc) { } async function next(body) { - let rdoc = await record.get(body.rid); + let rdoc = await record.get(body.domainId, body.rid); const $set = {}; const $push = {}; if (body.case) { @@ -50,12 +50,12 @@ async function next(body) { if (body.score) $set.score = body.score; if (body.time_ms) $set.time = body.time_ms; if (body.memory_kb) $set.memory = body.memory_kb; - rdoc = await record.update(body.rid, $set, $push); + rdoc = await record.update(body.domainId, body.rid, $set, $push); bus.publish('record_change', rdoc); } async function end(body) { - let rdoc = await record.get(body.rid); + let rdoc = await record.get(body.domainId, body.rid); const $set = {}; const $push = {}; if (body.case) { @@ -76,9 +76,9 @@ async function end(body) { if (body.memory_kb) $set.memory = body.memory_kb; $set.judgeAt = new Date(); $set.judger = body.judger; - rdoc = await record.update(body.rid, $set, $push); + rdoc = await record.update(body.domainId, body.rid, $set, $push); await _postJudge(rdoc); - rdoc = await record.update(body.rid, $set, $push); + rdoc = await record.update(body.domainId, body.rid, $set, $push); } class JudgeHandler extends Handler { diff --git a/hydro/handler/manage.js b/hydro/handler/manage.js index da91486a3..881aa6956 100644 --- a/hydro/handler/manage.js +++ b/hydro/handler/manage.js @@ -8,9 +8,9 @@ const loader = require('../loader'); const { RoleAlreadyExistError, ValidationError } = require('../error'); class ManageHandler extends Handler { - async prepare() { + async prepare({ domainId }) { this.checkPerm(PERM_MANAGE); - this.system = await user.getById(0); + this.system = await user.getById(domainId, 0); } } @@ -53,13 +53,13 @@ class ManageEditHandler extends ManageHandler { } class ManageUserHandler extends ManageHandler { - async get() { + async get({ domainId }) { const uids = []; const rudocs = {}; const [udocs, roles, sys] = await Promise.all([ - user.getMulti({ role: { $nin: ['default', 'guest'] } }).toArray(), - user.getRoles(), - user.getById(0), + user.getMulti(domainId, { role: { $nin: ['default', 'guest'] } }).toArray(), + user.getRoles(domainId), + user.getById(domainId, 0), ]); for (const role of roles) rudocs[role._id] = []; for (const udoc of udocs) { @@ -67,7 +67,7 @@ class ManageUserHandler extends ManageHandler { rudocs[udoc.role].push(udoc); } const rolesSelect = roles.map((role) => [role._id, role._id]); - const udict = await user.getList(uids); + const udict = await user.getList(domainId, uids); const path = [ ['Hydro', '/'], ['manage', '/manage'], @@ -86,10 +86,10 @@ class ManageUserHandler extends ManageHandler { } class ManagePermissionHandler extends ManageHandler { - async get() { + async get({ domainId }) { const [roles, sys] = await Promise.all([ - user.getRoles(), - user.getById(0), + user.getRoles(domainId), + user.getById(domainId, 0), ]); const path = [ ['Hydro', '/'], @@ -114,10 +114,10 @@ class ManagePermissionHandler extends ManageHandler { } class ManageRoleHandler extends ManageHandler { - async get() { + async get({ domainId }) { const [roles, sys] = await Promise.all([ - user.getRoles(), - user.getById(0), + user.getRoles(domainId), + user.getById(domainId, 0), ]); const path = [ ['Hydro', '/'], diff --git a/hydro/handler/problem.js b/hydro/handler/problem.js index bbd476f44..8e2efdc64 100644 --- a/hydro/handler/problem.js +++ b/hydro/handler/problem.js @@ -28,19 +28,21 @@ class ProblemHandler extends Handler { this.checkPerm(PERM_VIEW_PROBLEM); } - async get({ page = 1, category = '' }) { + async get({ domainId, page = 1, category = '' }) { this.response.template = 'problem_main.html'; const q = {}; let psdict = {}; if (category) q.$or = [{ category }, { tag: category }]; if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; const [pdocs, ppcount, pcount] = await paginate( - problem.getMulti(q).sort({ pid: 1 }), + problem.getMulti(domainId, q).sort({ pid: 1 }), page, await system.get('PROBLEM_PER_PAGE'), ); if (this.user.hasPerm(PERM_LOGGEDIN)) { - psdict = await problem.getListStatus(this.user._id, pdocs.map((pdoc) => pdoc._id)); + psdict = await problem.getListStatus( + domainId, this.user._id, pdocs.map((pdoc) => pdoc.docId), + ); } const path = [ ['Hydro', '/'], @@ -75,7 +77,7 @@ class ProblemHandler extends Handler { } class ProblemCategoryHandler extends ProblemHandler { - async get({ page = 1, category }) { + async get({ domainId, page = 1, category }) { this.response.template = 'problem_main.html'; const q = { $or: [{ category }, { tag: category }], @@ -83,12 +85,14 @@ class ProblemCategoryHandler extends ProblemHandler { let psdict = {}; if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; const [pdocs, ppcount, pcount] = await paginate( - problem.getMulti(q).sort({ pid: 1 }), + problem.getMulti(domainId, q).sort({ pid: 1 }), page, await system.get('PROBLEM_PER_PAGE'), ); if (this.user.hasPerm(PERM_LOGGEDIN)) { - psdict = await problem.getListStatus(this.user._id, pdocs.map((pdoc) => pdoc._id)); + psdict = await problem.getListStatus( + domainId, this.user._id, pdocs.map((pdoc) => pdoc.docId), + ); } const path = [ ['Hydro', '/'], @@ -102,10 +106,10 @@ class ProblemCategoryHandler extends ProblemHandler { } class ProblemRandomHandler extends ProblemHandler { - async get() { + async get({ domainId }) { const q = {}; if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; - const pid = await problem.random(q); + const pid = await problem.random(domainId, q); if (!pid) throw new NoProblemError(); this.response.body = { pid }; this.response.redirect = `/p/${pid}`; @@ -113,15 +117,17 @@ class ProblemRandomHandler extends ProblemHandler { } class ProblemDetailHandler extends ProblemHandler { - async _prepare({ pid }) { + async _prepare({ domainId, pid }) { this.response.template = 'problem_detail.html'; this.uid = this.user._id; this.pid = pid; - if (pid) this.pdoc = await problem.get(pid, this.uid); - if (this.pdoc.hidden && this.pdoc.owner !== this.uid) { - this.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); + if (pid) { + this.pdoc = await problem.get(domainId, pid, this.uid); + if (this.pdoc.hidden && this.pdoc.owner !== this.uid) { + this.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); + } + if (this.pdoc) this.udoc = await user.getById(domainId, this.pdoc.owner); } - if (this.pdoc) this.udoc = await user.getById(this.pdoc.owner); this.response.body = { pdoc: this.pdoc, udoc: this.udoc, @@ -143,9 +149,9 @@ class ProblemSubmitHandler extends ProblemDetailHandler { this.checkPerm(PERM_SUBMIT_PROBLEM); } - async get() { + async get({ domainId }) { this.response.template = 'problem_submit.html'; - const rdocs = await record.getUserInProblemMulti(this.uid, this.pdoc._id) + const rdocs = await record.getUserInProblemMulti(domainId, this.uid, this.pdoc.docId) .sort({ _id: -1 }) .limit(10) .toArray(); @@ -163,13 +169,13 @@ class ProblemSubmitHandler extends ProblemDetailHandler { }; } - async post() { + async post({ domainId }) { const { lang, code } = this.request.body; - const rid = await record.add({ - uid: this.uid, lang, code, pid: this.pdoc._id, + const rid = await record.add(domainId, { + uid: this.uid, lang, code, pid: this.pdoc.docId, }); await Promise.all([ - record.judge(rid), + record.judge(domainId, rid), user.inc(this.user._id, 'nSubmit'), ]); this.response.body = { rid }; @@ -178,25 +184,32 @@ class ProblemSubmitHandler extends ProblemDetailHandler { } class ProblemPretestHandler extends ProblemDetailHandler { - async post({ lang, code, input }) { + async post({ + domainId, lang, code, input, + }) { this.limitRate('add_record', 60, 100); - const rid = await record.add({ - uid: this.uid, lang, code, pid: this.pdoc._id, input, + const rid = await record.add(domainId, { + uid: this.uid, lang, code, pid: this.pdoc.docId, input, }); - await record.judge(rid); + await record.judge(domainId, rid); this.response.body = { rid }; } } class ProblemPretestConnectionHandler extends ConnectionHandler { - async prepare({ pid }) { + async prepare({ domainId, pid }) { this.pid = pid.toString(); + this.domainId = domainId; bus.subscribe(['record_change'], this.onRecordChange); } async onRecordChange(data) { const rdoc = data.value; - if (rdoc.uid !== this.user._id || rdoc.pid.toString() !== this.pid) return; + if ( + rdoc.uid !== this.user._id + || rdoc.pid.toString() !== this.pid + || rdoc.domainId !== this.domainId + ) return; if (rdoc.tid) return; this.send({ rdoc }); } @@ -207,8 +220,8 @@ class ProblemPretestConnectionHandler extends ConnectionHandler { } class ProblemStatisticsHandler extends ProblemDetailHandler { - async get() { - const udoc = await user.getById(this.pdoc.owner); + async get({ domainId }) { + const udoc = await user.getById(domainId, this.pdoc.owner); const path = [ ['problem_main', '/p'], [this.pdoc.title, `/p/${this.pdoc.pid}`, true], @@ -254,10 +267,10 @@ class ProblemEditHandler extends ProblemManageHandler { this.response.body.page_name = 'problem_edit'; } - async post({ title, content }) { + async post({ domainId, title, content }) { const pid = validator.checkPid(this.request.body.pid); - const pdoc = await problem.get(this.params.pid); - await problem.edit(pdoc._id, { title, content, pid }); + const pdoc = await problem.get(domainId, this.params.pid); + await problem.edit(domainId, pdoc.docId, { title, content, pid }); this.response.redirect = `/p/${pid}`; } } @@ -275,10 +288,10 @@ class ProblemDataUploadHandler extends ProblemManageHandler { this.response.body.md5 = this.md5; } - async post() { + async post({ domainId }) { if (!this.request.files.file) throw new BadRequestError(); const r = fs.createReadStream(this.request.files.file.path); - await problem.setTestdata(this.pdoc._id, r); + await problem.setTestdata(domainId, this.pdoc.docId, r); if (this.pdoc.data && typeof this.pdoc.data === 'object') { const f = await file.getMeta(this.pdoc.data); this.md5 = f.md5; @@ -297,25 +310,25 @@ class ProblemDataDownloadHandler extends ProblemDetailHandler { } class ProblemSolutionHandler extends ProblemDetailHandler { - async get({ page = 1 }) { + async get({ domainId, page = 1 }) { this.response.template = 'problem_solution.html'; this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); const [psdocs, pcount, pscount] = await paginate( - solution.getMulti(this.pdoc._id), + solution.getMulti(domainId, this.pdoc.docId), page, await system.get('SOLUTION_PER_PAGE'), ); const uids = [this.pdoc.owner]; const docids = []; for (const psdoc of psdocs) { - docids.push(psdoc._id); + docids.push(psdoc.docId); uids.push(psdoc.owner); if (psdoc.reply.length) { for (const psrdoc of psdoc.reply) uids.push(psrdoc.owner); } } const udict = await user.getList(uids); - const pssdict = solution.getListStatus(docids, this.uid); + const pssdict = solution.getListStatus(domainId, docids, this.uid); const path = [ ['problem_main', '/p'], [this.pdoc.title, `/p/${this.pdoc.pid}`, true], @@ -326,77 +339,79 @@ class ProblemSolutionHandler extends ProblemDetailHandler { }; } - async post({ psid }) { - if (psid) this.psdoc = await solution.get(psid); + async post({ domainId, psid }) { + if (psid) this.psdoc = await solution.get(domainId, psid); } - async postSubmit({ content }) { + async postSubmit({ domainId, content }) { this.checkPerm(PERM_CREATE_PROBLEM_SOLUTION); - await solution.add(this.pdoc._id, this.uid, content); + await solution.add(domainId, this.pdoc.docId, this.uid, content); this.back(); } - async postEditSolution({ content }) { + async postEditSolution({ domainId, content }) { if (this.psdoc.owner !== this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION); - this.psdoc = await solution.edit(this.psdoc._id, content); + this.psdoc = await solution.edit(domainId, this.psdoc.docId, content); this.ctx.body.psdoc = this.psdoc; this.back(); } - async postDeleteSolution() { + async postDeleteSolution({ domainId }) { if (this.psdoc.owner !== this.uid) this.checkPerm(PERM_DELETE_PROBLEM_SOLUTION); - await solution.del(this.psdoc._id); + await solution.del(domainId, this.psdoc.docId); this.back(); } - async postReply({ psid, content }) { + async postReply({ domainId, psid, content }) { this.checkPerm(PERM_REPLY_PROBLEM_SOLUTION); - const psdoc = await solution.get(psid); - await solution.reply(psdoc._id, this.uid, content); + const psdoc = await solution.get(domainId, psid); + await solution.reply(domainId, psdoc.docId, this.uid, content); } - async postEditReply({ content, psid, psrid }) { - const [psdoc, psrdoc] = await solution.getReply(psid, psrid); - if ((!psdoc) || psdoc.pid !== this.pdoc._id) throw new SolutionNotFoundError(psid); + async postEditReply({ + domainId, content, psid, psrid, + }) { + const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid); + if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid); if (psrdoc.owner !== this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY); - await solution.editReply(psid, psrid, content); + await solution.editReply(domainId, psid, psrid, content); } - async postDeleteReply({ psid, psrid }) { - const [psdoc, psrdoc] = await solution.getReply(psid, psrid); - if ((!psdoc) || psdoc.pid !== this.pdoc._id) throw new SolutionNotFoundError(psid); + async postDeleteReply({ domainId, psid, psrid }) { + const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid); + if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid); if (psrdoc.owner !== this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY); - await solution.delReply(psid, psrid); + await solution.delReply(domainId, psid, psrid); this.back(); } - async postUpvote() { - const [psdoc, pssdoc] = await solution.vote(this.psdoc._id, this.uid, 1); + async postUpvote({ domainId }) { + const [psdoc, pssdoc] = await solution.vote(domainId, this.psdoc.docId, this.uid, 1); this.response.body = { vote: psdoc.vote, user_vote: pssdoc.vote }; - if (!this.preferJson) this.back(); + this.back(); } - async postDownvote() { - const [psdoc, pssdoc] = await solution.vote(this.psdoc._id, this.uid, -1); + async postDownvote({ domainId }) { + const [psdoc, pssdoc] = await solution.vote(domainId, this.psdoc.docId, this.uid, -1); this.response.body = { vote: psdoc.vote, user_vote: pssdoc.vote }; - if (!this.preferJson) this.back(); + this.back(); } } class ProblemSolutionRawHandler extends ProblemDetailHandler { - async get({ psid }) { + async get({ domainId, psid }) { this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); - const psdoc = await solution.get(psid); + const psdoc = await solution.get(domainId, psid); this.response.type = 'text/markdown'; this.response.body = psdoc.content; } } class ProblemSolutionReplyRawHandler extends ProblemDetailHandler { - async get({ psid }) { + async get({ domainId, psid }) { this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); - const [psdoc, psrdoc] = await solution.getReply(psid); - if ((!psdoc) || psdoc.pid !== this.pdoc._id) throw new SolutionNotFoundError(psid); + const [psdoc, psrdoc] = await solution.getReply(domainId, psid); + if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid); this.response.type = 'text/markdown'; this.response.body = psrdoc.content; } @@ -417,12 +432,10 @@ class ProblemCreateHandler extends Handler { } async post({ - title, pid, content, hidden, + domainId, title, pid, content, hidden, }) { - validator.checkPid(pid); - pid = pid || await system.inc('pid'); - await problem.add({ - title, content, owner: this.user._id, pid, hidden, + pid = await problem.add(domainId, title, content, this.user._id, { + pid, hidden, }); this.response.body = { pid }; this.response.redirect = `/p/${pid}/settings`; diff --git a/hydro/handler/record.js b/hydro/handler/record.js index 12a977745..0116199a4 100644 --- a/hydro/handler/record.js +++ b/hydro/handler/record.js @@ -13,13 +13,13 @@ const { } = require('../service/server'); class RecordListHandler extends Handler { - async get({ page = 1 }) { + async get({ domainId, page = 1 }) { this.response.template = 'record_main.html'; const q = {}; - const rdocs = await record.getMany(q, { _id: -1 }, page, await system.get('RECORD_PER_PAGE')); + const rdocs = await record.getMany(domainId, q, { _id: -1 }, page, await system.get('RECORD_PER_PAGE')); const [udict, pdict] = await Promise.all([ - user.getList(rdocs.map((rdoc) => rdoc.uid)), - problem.getList(rdocs.map((rdoc) => rdoc.pid)), + user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)), + problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid)), ]); const path = [ ['Hydro', '/'], @@ -32,19 +32,23 @@ class RecordListHandler extends Handler { } class RecordDetailHandler extends Handler { - async get({ rid }) { + async get({ domainId, rid }) { this.response.template = 'record_detail.html'; - const rdoc = await record.get(rid); + const rdoc = await record.get(domainId, rid); if (rdoc.hidden) this.checkPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM_READ_RECORD_CODE)) { rdoc.code = null; } - const pdoc = await problem.getById(rdoc.pid); + const [pdoc, udoc] = await Promise.all([ + problem.get(domainId, rdoc.pid), + user.getById(domainId, rdoc.uid), + ]); this.response.body = { path: [ ['Hydro', '/'], ['record_detail', null], ], + udoc, rdoc, pdoc, show_status: true, @@ -53,16 +57,17 @@ class RecordDetailHandler extends Handler { } class RecordRejudgeHandler extends Handler { - async post({ rid }) { + async post({ domainId, rid }) { this.checkPerm(PERM_REJUDGE); - const rdoc = await record.get(rid); - if (rdoc) await record.rejudge(rid); + const rdoc = await record.get(domainId, rid); + if (rdoc) await record.rejudge(domainId, rid); this.back(); } } class RecordConnectionHandler extends ConnectionHandler { - async prepare() { + async prepare({ domainId }) { + this.domainId = domainId; bus.subscribe(['record_change'], this.onRecordChange); } @@ -81,17 +86,20 @@ class RecordConnectionHandler extends ConnectionHandler { const rdoc = data.value; if (rdoc.tid && rdoc.tid.toString() !== this.tid) return; // eslint-disable-next-line prefer-const - let [udoc, pdoc] = await Promise.all([user.getById(rdoc.uid), problem.getById(rdoc.pid)]); + let [udoc, pdoc] = await Promise.all([ + user.getById(this.domainId, rdoc.uid), + problem.getById(this.domainId, rdoc.pid), + ]); if (pdoc.hidden && !this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null; this.send({ html: await this.renderHTML('record_main_tr.html', { rdoc, udoc, pdoc }) }); } } class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(ConnectionHandler) { - async prepare({ rid }) { - const rdoc = await record.get(rid); + async prepare({ domainId, rid }) { + const rdoc = await record.get(domainId, rid); if (rdoc.tid) { - const tdoc = await contest.get(rdoc.tid); + const tdoc = await contest.get(domainId, rdoc.tid); if (!this.canShowRecord(tdoc)) { this.close(); return; diff --git a/hydro/handler/training.js b/hydro/handler/training.js index 993fee713..4d708608a 100644 --- a/hydro/handler/training.js +++ b/hydro/handler/training.js @@ -32,7 +32,7 @@ async function _parseDagJson(dag) { // eslint-disable-next-line no-await-in-loop const pdoc = await problem.get(node.pids[i]); // FIXME no-await-in-loop assert(pdoc, `Problem not found: ${node.pids[i]}`); - node.pids[i] = pdoc._id; + node.pids[i] = pdoc.docId; } const newNode = { _id: parseInt(node._id), @@ -63,7 +63,7 @@ class TrainingMainHandler extends TrainingHandler { await system.get('TRAINING_PER_PAGE'), ); const tids = new Set(); - for (const tdoc of tdocs) tids.add(tdoc._id); + for (const tdoc of tdocs) tids.add(tdoc.docId); const tsdict = {}; let tdict = {}; if (this.user.hasPerm(PERM_LOGGEDIN)) { @@ -79,7 +79,7 @@ class TrainingMainHandler extends TrainingHandler { for (const tid of tids) enrolledTids.delete(tid); if (enrolledTids.size) tdict = await training.getList(Array.from(enrolledTids)); } - for (const tdoc in tdocs) tdict[tdoc._id] = tdoc; + for (const tdoc in tdocs) tdict[tdoc.docId] = tdoc; const path = [ ['Hydro', '/'], ['training_main', null], @@ -92,16 +92,16 @@ class TrainingMainHandler extends TrainingHandler { } class TrainingDetailHandler extends TrainingHandler { - async get({ tid }) { - const tdoc = await training.get(tid); + async get({ domainId, tid }) { + const tdoc = await training.get(domainId, tid); const pids = training.getPids(tdoc); // TODO(twd2): check status, eg. test, hidden problem, ... const f = this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN) ? {} : { hidden: false }; const [udoc, pdict] = await Promise.all([ - user.getById(tdoc.owner), - problem.getList(pids, f), + user.getById(domainId, tdoc.owner), + problem.getList(domainId, pids, f), ]); - const psdict = await problem.getListStatus(this.user._id, Object.keys(pdict)); + const psdict = await problem.getListStatus(domainId, this.user._id, Object.keys(pdict)); const donePids = new Set(); const progPids = new Set(); for (const pid in psdict) { @@ -128,7 +128,7 @@ class TrainingDetailHandler extends TrainingHandler { if (nsdoc.isDone) doneNids.add(node._id); nsdict[node._id] = nsdoc; } - const tsdoc = await training.setStatus(tdoc._id, this.user._id, { + const tsdoc = await training.setStatus(domainId, tdoc.docId, this.user._id, { doneNids: Array.from(doneNids), donePids: Array.from(donePids), done: doneNids.size === tdoc.dag.length, @@ -143,10 +143,10 @@ class TrainingDetailHandler extends TrainingHandler { }; } - async postEnroll({ tid }) { + async postEnroll({ domainId, tid }) { this.checkPerm(PERM_LOGGEDIN); - const tdoc = await training.get(tid); - await training.enroll(tdoc._id, this.user._id); + const tdoc = await training.get(domainId, tid); + await training.enroll(domainId, tdoc.docId, this.user._id); this.back(); } } @@ -176,7 +176,7 @@ class TrainingCreateHandler extends TrainingHandler { const pdocs = await problem.getMulti({ $or: [{ _id: { $in: pids } }, { pid: { $in: pids } }], }).sort('_id', 1).toArray(); - const existPids = pdocs.map((pdoc) => pdoc._id); + const existPids = pdocs.map((pdoc) => pdoc.docId); const existPnames = pdocs.map((pdoc) => pdoc.pid); if (pids.length !== existPids.length) { for (const pid in pids) { @@ -202,7 +202,7 @@ class TrainingEditHandler extends TrainingHandler { const dag = JSON.stringify(tdoc.dag, null, 2); const path = [ ['training_main', '/t'], - [tdoc.title, `/t/${tdoc._id}`, true], + [tdoc.title, `/t/${tdoc.docId}`, true], ['training_edit', null], ]; this.response.template = 'training_edit.html'; @@ -225,7 +225,7 @@ class TrainingEditHandler extends TrainingHandler { { pid: { $in: pids } }, ], }).sort('_id', 1).toArray(); - const existPids = pdocs.map((pdoc) => pdoc._id); + const existPids = pdocs.map((pdoc) => pdoc.docId); const existPnames = pdocs.map((pdoc) => pdoc.pid); if (pids.length !== existPids.length) { for (const pid in pids) { diff --git a/hydro/handler/user.js b/hydro/handler/user.js index 9adc33c9b..d21e46245 100644 --- a/hydro/handler/user.js +++ b/hydro/handler/user.js @@ -15,8 +15,10 @@ class UserLoginHandler extends Handler { this.response.template = 'user_login.html'; } - async post({ uname, password, rememberme = false }) { - const udoc = await user.getByUname(uname); + async post({ + domainId, uname, password, rememberme = false, + }) { + const udoc = await user.getByUname(domainId, uname); if (!udoc) throw new LoginError(uname); if (udoc) udoc.checkPassword(password); await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip }); @@ -120,10 +122,10 @@ class UserLostPassHandler extends Handler { } class UserLostPassWithCodeHandler extends Handler { - async get({ code }) { + async get({ domainId, code }) { const tdoc = await token.get(code, token.TYPE_LOSTPASS); if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code); - const udoc = await user.getById(tdoc.uid); + const udoc = await user.getById(domainId, tdoc.uid); this.response.body = { uname: udoc.uname }; } @@ -138,9 +140,9 @@ class UserLostPassWithCodeHandler extends Handler { } class UserDetailHandler extends Handler { - async get({ uid }) { + async get({ domainId, uid }) { const isSelfProfile = this.user._id === uid; - const udoc = await user.getById(uid); + const udoc = await user.getById(domainId, uid); if (!udoc) throw new UserNotFoundError(uid); const sdoc = await token.getMostRecentSessionByUid(uid); this.response.template = 'user_detail.html'; @@ -149,12 +151,12 @@ class UserDetailHandler extends Handler { } class UserSearchHandler extends Handler { - async get({ q, exactMatch = false }) { + async get({ domainId, q, exactMatch = false }) { let udocs; if (exactMatch) udocs = []; else udocs = await user.getPrefixList(q, 20); try { - const udoc = await user.getById(parseInt(q)); + const udoc = await user.getById(domainId, parseInt(q)); if (udoc) udocs.push(udoc); } catch (e) { /* Ignore */ diff --git a/hydro/interface.js b/hydro/interface.js index 4d093da3f..3075b3b39 100644 --- a/hydro/interface.js +++ b/hydro/interface.js @@ -20,6 +20,7 @@ exports.udoc = { viewLang: 'zh_CN', loginat: new Date(), loginip: '0.0.0.0', + priv: 0, }; exports.pdoc = { diff --git a/hydro/loader.js b/hydro/loader.js index 4be81476f..23e73d55a 100644 --- a/hydro/loader.js +++ b/hydro/loader.js @@ -238,13 +238,13 @@ async function load() { await server.prepare(); await service(); const builtinModel = [ - 'blacklist', 'builtin', 'contest', 'discussion', 'message', + 'document', 'blacklist', 'builtin', 'contest', 'message', 'opcount', 'problem', 'record', 'setting', 'solution', - 'token', 'training', 'user', 'file', + 'token', 'training', 'user', 'file', 'discussion', ]; for (const i of builtinModel) { const m = require(`./model/${i}`); - if (m.index) await m.index(); + if (m.ensureIndexes) await m.ensureIndexes(); } const system = require('./model/system'); const dbVer = await system.get('db.ver'); @@ -268,6 +268,10 @@ async function load() { for (const i in global.Hydro.service) { if (global.Hydro.service[i].postInit) await global.Hydro.service[i].postInit(); } + const builtinScript = [ + 'install', 'uninstall', + ]; + for (const i of builtinScript) require(`./script/${i}`); await script(); pending = []; await server.start(); diff --git a/hydro/model/blacklist.js b/hydro/model/blacklist.js index 95be826c4..ac46dfaca 100644 --- a/hydro/model/blacklist.js +++ b/hydro/model/blacklist.js @@ -15,10 +15,10 @@ function del(ip) { return coll.deleteOne({ _id: ip }); } -function index() { +function ensureIndexes() { return coll.createIndex('expireAt', { expireAfterSeconds: 0 }); } global.Hydro.model.blacklist = module.exports = { - add, get, del, index, + add, get, del, ensureIndexes, }; diff --git a/hydro/model/builtin.js b/hydro/model/builtin.js index eb2d45dcf..0f46d5832 100644 --- a/hydro/model/builtin.js +++ b/hydro/model/builtin.js @@ -1,10 +1,10 @@ const perm = require('../permission'); -const BUILTIN_ROLES = [ - { _id: 'guest', perm: perm.PERM_BASIC }, - { _id: 'default', perm: perm.PERM_DEFAULT, count: 0 }, - { _id: 'root', perm: perm.PERM_ALL, count: 0 }, -]; +const BUILTIN_ROLES = { + guest: { perm: perm.PERM_BASIC }, + default: { perm: perm.PERM_DEFAULT }, + admin: { perm: perm.PERM_ADMIN }, +}; const DEFAULT_NODES = { 探索: [ diff --git a/hydro/model/contest.js b/hydro/model/contest.js index cfdc5124c..01547f284 100644 --- a/hydro/model/contest.js +++ b/hydro/model/contest.js @@ -7,10 +7,7 @@ const { const { PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD } = require('../permission'); const validator = require('../lib/validator'); const ranked = require('../lib/rank'); -const db = require('../service/db'); - -const coll = db.collection('contest'); -const collStatus = db.collection('contest.status'); +const document = require('./document'); const acm = { TEXT: 'ACM/ICPC', @@ -197,6 +194,7 @@ const RULES = { */ /** + * @param {string} domainId * @param {string} title * @param {string} content * @param {number} owner @@ -207,7 +205,7 @@ const RULES = { * @param {object} data * @returns {ObjectID} tid */ -async function add(title, content, owner, rule, +function add(domainId, title, content, owner, rule, beginAt = new Date(), endAt = new Date(), pids = [], data = {}) { validator.checkTitle(title); validator.checkContent(content); @@ -217,15 +215,18 @@ async function add(title, content, owner, rule, content, owner, title, rule, beginAt, endAt, pids, attend: 0, }); this.RULES[rule].check(data); - const res = await coll.insertOne(data); - return res.insertedId; + return document.add(domainId, content, owner, document.TYPE_CONTEST, null, null, null, { + ...data, title, rule, beginAt, endAt, pids, attend: 0, + }); } + /** + * @param {string} domainId * @param {ObjectID} tid * @param {object} $set * @returns {Tdoc} tdoc after modification */ -async function edit(tid, $set) { +async function edit(domainId, tid, $set) { if ($set.title) validator.checkTitle($set.title); if ($set.content) validator.checkIntro($set.content); if ($set.rule) { if (!this.RULES[$set.rule]) throw new ValidationError('rule'); } @@ -234,53 +235,55 @@ async function edit(tid, $set) { throw new ValidationError('beginAt', 'endAt'); } } - const tdoc = await coll.findOne({ tid }); + const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); if (!tdoc) throw new ContestNotFoundError(tid); this.RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set)); - await coll.findOneAndUpdate({ tid }, { $set }); - return tdoc; + return await document.set(domainId, document.TYPE_CONTEST, tid, $set); } + /** + * @param {string} domainId * @param {ObjectID} tid * @returns {Tdoc} */ -async function get(tid) { - const tdoc = await coll.findOne({ _id: tid }); +async function get(domainId, tid) { + const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); if (!tdoc) throw new ContestNotFoundError(tid); return tdoc; } -async function updateStatus(tid, uid, rid, pid, accept, score) { - const tdoc = await get(tid); - const tsdoc = await collStatus.findOneAndUpdate({ tid: tdoc._id, uid }, { - $push: { - journal: { - rid, pid, accept, score, - }, - }, - $inc: { rev: 1 }, - }, { upsert: true }); +async function updateStatus(domainId, tid, uid, rid, pid, accept, score) { + await get(domainId, tid); + const tsdoc = await document.revPushStatus(domainId, document.TYPE_CONTEST, tid, uid, 'journal', { + rid, pid, accept, score, + }); if (!tsdoc.value.attend) throw new ContestNotAttendedError(tid, uid); } -function getStatus(tid, uid) { - return collStatus.findOne({ tid, uid }); + +function getStatus(domainId, tid, uid) { + return document.getStatus(domainId, tid, uid); } -async function getListStatus(uid, tids) { + +async function getListStatus(domainId, uid, tids) { const r = {}; // eslint-disable-next-line no-await-in-loop - for (const tid of tids) r[tid] = await getStatus(tid, uid); + for (const tid of tids) r[tid] = await getStatus(domainId, tid, uid); return r; } -async function attend(tid, uid) { + +async function attend(domainId, tid, uid) { try { - await collStatus.insertOne({ tid, uid, attend: 1 }); + await document.cappedIncStatus(domainId, document.TYPE_CONTEST, tid, uid, 'attend', 1, 0, 1); } catch (e) { throw new ContestAlreadyAttendedError(tid, uid); } - await coll.findOneAndUpdate({ _id: tid }, { $inc: { attend: 1 } }); + await document.inc(domainId, document, document.TYPE_CONTEST, tid, 'attend', 1); + return {}; } + function getMultiStatus(query) { - return collStatus.find(query); + return document.getMultiStatus(query); } + function isNew(tdoc, days = 1) { const now = new Date().getTime(); const readyAt = tdoc.beginAt.getTime(); @@ -319,29 +322,37 @@ const ContestHandlerMixin = (c) => class extends c { return false; } - async getScoreboard(tid, isExport = false) { - const tdoc = await get(tid); + async getScoreboard(domainId, tid, isExport = false) { + const tdoc = await get(domainId, tid); if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid); - const tsdocs = await getMultiStatus(tid).sort(RULES[tdoc.rule].statusSort).toArray(); + const tsdocs = await getMultiStatus(domainId, tid) + .sort(RULES[tdoc.rule].statusSort).toArray(); const uids = []; for (const tsdoc of tsdocs) uids.push(tsdoc.uid); - const [udict, pdict] = await Promise.all([user.getList(uids), problem.getList(tdoc.pids)]); + const [udict, pdict] = await Promise.all([ + user.getList(uids), + problem.getList(domainId, tdoc.pids), + ]); const rankedTsdocs = RULES[tdoc.rule].rank(tsdocs); const rows = RULES[tdoc.rule].scoreboard(isExport, (str) => (str ? str.toString().translate(this.user.language) : ''), tdoc, rankedTsdocs, udict, pdict); return [tdoc, rows, udict]; } - async verifyProblems(pids) { // eslint-disable-line class-methods-use-this + async verifyProblems(domainId, pids) { // eslint-disable-line class-methods-use-this const r = []; for (const pid of pids) { - const res = await problem.get(pid); // eslint-disable-line no-await-in-loop - if (res) r.push(res._id); + const res = await problem.get(domainId, pid); // eslint-disable-line no-await-in-loop + if (res) r.push(res.docId); else throw new ProblemNotFoundError(pid); } return r; } }; +function setStatus(domainId, tid, uid, $set) { + return document.setStatus(domainId, document.TYPE_CONTEST, tid, uid, $set); +} + global.Hydro.model.contest = module.exports = { RULES, ContestHandlerMixin, @@ -352,9 +363,9 @@ global.Hydro.model.contest = module.exports = { get, updateStatus, getStatus, - count: (query) => coll.find(query).count(), - getMulti: (query) => coll.find(query), - setStatus: (tid, uid, $set) => collStatus.findOneAndUpdate({ tid, uid }, { $set }), + count: (query) => document.count({ ...query, docType: document.TYPE_CONTEST }), + getMulti: (query = {}) => document.getMulti({ ...query, docType: document.TYPE_CONTEST }), + setStatus, isNew, isUpcoming, isNotStarted, @@ -390,7 +401,7 @@ async def recalc_status(domainId: str, doc_type: int, cid: objeccid.Objeccid): tdoc = await document.get(domainId, doc_type, cid) async with document.get_multi_status(domainId=domainId, doc_type=doc_type, - doc_id=tdoc._id) as tsdocs: + doc_id=tdoc.docId) as tsdocs: async for tsdoc in tsdocs: if 'journal' not in tsdoc or not tsdoc['journal']: continue diff --git a/hydro/model/discussion.js b/hydro/model/discussion.js index 8472a8bea..d17e10627 100644 --- a/hydro/model/discussion.js +++ b/hydro/model/discussion.js @@ -1,157 +1,140 @@ const { ObjectID } = require('bson'); const problem = require('./problem'); +const document = require('./document'); const contest = require('./contest'); const { PERM_VIEW_PROBLEM_HIDDEN } = require('../permission'); const { DocumentNotFoundError } = require('../error'); -const db = require('../service/db'); -const coll = db.collection('discussion'); -const collReply = db.collection('discussion.reply'); -const collStatus = db.collection('discussion.status'); -const collNode = db.collection('discussion.node'); - -async function add(parentType, parentId, owner, title, content, ip = null, highlight = false) { - const res = await coll.insertOne({ - owner, - title, - content, - ip, - highlight, - parentType, - parentId, - nReply: 0, - updateAt: new Date(), - }); - return res.insertedId; +function add(domainId, parentType, parentId, owner, title, content, ip = null, highlight = false) { + return document.add( + domainId, content, owner, document.TYPE_DISCUSSION, + null, parentType, parentId, + { + title, + ip, + highlight, + nReply: 0, + updateAt: new Date(), + }, + ); } -function get(did) { - return coll.findOne({ _id: did }); +function get(domainId, did) { + return document.get(domainId, document.TYPE_DISCUSSION, did); } -function edit(did, title, content, highlight) { - return coll.findOneAndUpdate({ _id: did }, { $set: { title, content, highlight } }); +function edit(domainId, did, title, content, highlight) { + return document.set(domainId, document.TYPE_DISCUSSION, did, { title, content, highlight }); } -function del(did) { +function del(domainId, did) { + // TODO(masnn) delete status return Promise.all([ - coll.deleteOne({ _id: did }), - collReply.deleteMany({ did }), - collStatus.deleteMany({ did }), + document.deleteOne(domainId, document.TYPE_DISCUSSION, did), + document.deleteMulti(domainId, document.TYPE_DISCUSSION_REPLY, { + parentType: document.TYPE_DISCUSSION, parentId: did, + }), ]); } -function count(query) { - return coll.find(query).count(); +function count(domainId, query) { + return document.count(domainId, document.TYPE_DISCUSSION, query); } -function getMulti(query) { - return coll.find(query).sort('updateAt', -1); +function getMulti(domainId, query) { + return document.getMulti(domainId, document.TYPE_DISCUSSION, query).sort('updateAt', -1); } -async function addReply(did, owner, content, ip) { +async function addReply(domainId, did, owner, content, ip) { const [drdoc] = await Promise.all([ - collReply.insertOne({ - owner, did, content, ip, - }), - coll.updateOne({ _id: did }, { $inc: { nReply: 1 }, $set: { updateAt: new Date() } }), + document.add( + domainId, content, owner, document.TYPE_DISCUSSION_REPLY, + null, document.TYPE_DISCUSSION, did, { ip }, + ), + document.incAndSet(domainId, document.TYPE_DISCUSSION, did, 'nReply', 1, { updateAt: new Date() }), ]); return drdoc; } -function getReply(drid) { - return collReply.findOne({ _id: drid }); +function getReply(domainId, drid) { + return document.get(domainId, document.TYPE_DISCUSSION_REPLY, drid); } -function editReply(drid, content) { - return collReply.updateOne({ _id: drid }, { $set: { content } }); +function editReply(domainId, drid, content) { + return document.set(domainId, document.TYPE_DISCUSSION_REPLY, drid, { content }); } -async function delReply(drid) { - const drdoc = await getReply(drid); +async function delReply(domainId, drid) { + const drdoc = await getReply(domainId, drid); if (!drdoc) throw new DocumentNotFoundError(drid); return await Promise.all([ // eslint-disable-line no-return-await - collReply.deleteOne({ _id: drid }), - coll.updateOne({ _id: drdoc.did }, { $inc: { nReply: -1 } }), + document.deleteOne(domainId, document.TYPE_DISCUSSION_REPLY, drid), + document.inc(domainId, document.TYPE_DISCUSSION, drdoc.parentId, 'nReply', -1), ]); } -function getMultiReply(did) { - return coll.find({ did }).sort('_id', -1); +function getMultiReply(domainId, did) { + return document.getMulti( + domainId, document.TYPE_DISCUSSION_REPLY, + { parentType: document.TYPE_DISCUSSION, parentId: did }, + ).sort('_id', -1); } -function getListReply(did) { - return getMultiReply({ did }).toArray(); +function getListReply(domainId, did) { + return getMultiReply(domainId, did).toArray(); } -async function addTailReply(drid, owner, content, ip) { - let drdoc = await collReply.findOne({ _id: drid }); +async function addTailReply(domainId, drid, owner, content, ip) { + let drdoc = await document.get(domainId, document.TYPE_DISCUSSION_REPLY, drid); const sid = new ObjectID(); - await Promise.all([ - collReply.updateOne({ _id: drid }, { - $push: { - reply: { - _id: sid, content, owner, ip, - }, - }, - }), - coll.updateOne({ _id: drdoc.did }, { $set: { updateAt: new Date() } }), + [drdoc] = await Promise.all([ + document.push(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', content, owner, { ip }), + document.set(domainId, document.TYPE_DISCUSSION, drdoc.parentId, { updateAt: new Date() }), ]); - drdoc = await collReply.findOne({ _id: drid }); return [drdoc, sid]; } -async function getTailReply(drid, drrid) { - const drdoc = await collReply.findOne({ _id: drid, reply: { $elemMatch: { _id: drrid } } }); - if (!drdoc) return [null, null]; - for (const drrdoc of drdoc) if (drrdoc._id === drrid) return [drdoc, drrdoc]; - return [drdoc, null]; +function getTailReply(domainId, drid, drrid) { + return document.getSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid); } -async function editTailReply(drid, drrid, content) { - const drdoc = await collReply.findOne({ _id: drid, reply: { $elemMatch: { _id: drrid } } }); - const { reply } = drdoc; - for (const i in reply) { - if (reply[i]._id === drrid) { - reply[i].content = content; - break; - } - } - // eslint-disable-next-line no-return-await - return await collReply.findOneAndUpdate({ _id: drdoc._id }, { $set: { reply } }); +function editTailReply(domainId, drid, drrid, content) { + return document.setSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid, { content }); } -function delTailReply(drid, drrid) { - return coll.findOneAndUpdate({ _id: drid }, { $pull: { reply: { _id: drrid } } }); +function delTailReply(domainId, drid, drrid) { + return document.deleteSub(domainId, document.TYPE_DISCUSSION_REPLY, drid, 'reply', drrid); } -function setStar(did, uid, star) { - return collStatus.findOneAndUpdate({ did, uid }, { $set: { star } }, { upsert: true }); +function setStar(domainId, did, uid, star) { + return document.setStatus(domainId, document.TYPE_DISCUSSION, did, uid, { star }); } -function getStatus(did, uid) { - return collStatus.findOne({ did, uid }); +function getStatus(domainId, did, uid) { + return document.getStatus(domainId, document.TYPE_DISCUSSION, did, uid); } -function addNode(_id, category) { - return collNode.insertOne({ _id, category }); +function addNode(domainId, _id, category) { + return document.add( + domainId, _id, 1, document.TYPE_DISCUSSION_NODE, null, null, null, { category }, + ); } -function getNode(_id) { - return collNode.findOne({ _id }); +function getNode(domainId, _id) { + return document.get(domainId, document.TYPE_DISCUSSION_NODE, _id); } -async function getVnode(ddoc, handler) { +async function getVnode(domainId, ddoc, handler) { if (ddoc.parentType === 'problem') { - const pdoc = await problem.getById(ddoc.parentId); + const pdoc = await problem.getById(domainId, ddoc.parentId); if (!pdoc) return null; if (pdoc.hidden && handler) handler.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); return { ...pdoc, type: ddoc.parentType, id: ddoc.parentId }; } if (ddoc.parentType === 'contest') { - const tdoc = await contest.get(ddoc.parentId); + const tdoc = await contest.get(domainId, ddoc.parentId); return { ...tdoc, type: ddoc.parentType, id: ddoc.parentId }; } if (ddoc.parentType === 'node') { - const ndoc = await getNode(ddoc.parentId); + const ndoc = await getNode(domainId, ddoc.parentId); return { title: ndoc._id, type: ddoc.parentType, id: ddoc.parentId }; } return { @@ -161,11 +144,11 @@ async function getVnode(ddoc, handler) { }; } -async function getListVnodes(ddocs, handler) { +async function getListVnodes(domainId, ddocs, handler) { const res = {}; for (const ddoc of ddocs) { // FIXME no-await-in-loop - res[ddoc._id] = await getVnode(ddoc, handler); // eslint-disable-line no-await-in-loop + res[ddoc._id] = await getVnode(domainId, ddoc, handler); // eslint-disable-line no-await-in-loop } return res; } diff --git a/hydro/model/document.js b/hydro/model/document.js new file mode 100644 index 000000000..556bb8c8a --- /dev/null +++ b/hydro/model/document.js @@ -0,0 +1,366 @@ +const assert = require('assert'); +const { ObjectID } = require('bson'); +const db = require('../service/db'); + +const coll = db.collection('document'); +const collStatus = db.collection('document.status'); + +const TYPE_DOMAIN_ROLE = 0; +const TYPE_PROBLEM = 10; +const TYPE_PROBLEM_SOLUTION = 11; +const TYPE_PROBLEM_LIST = 12; +const TYPE_DISCUSSION_NODE = 20; +const TYPE_DISCUSSION = 21; +const TYPE_DISCUSSION_REPLY = 22; +const TYPE_CONTEST = 30; +const TYPE_TRAINING = 40; +const TYPE_FILE = 50; +const TYPE_HOMEWORK = 60; + +async function add( + domainId, content, owner, docType, docId, + parentType = null, parentId = null, args = {}, +) { + const _id = new ObjectID(); + const doc = { + content, + owner, + domainId, + docType, + docId: docId || _id, + ...args, + }; + if (parentType || parentId) { + assert(parentType && parentId); + doc.parentType = parentType; + doc.parentId = parentId; + } + const res = await coll.insertOne(doc); + return docId || res.insertedId; +} + +function get(domainId, docType, docId) { + return coll.findOne({ domainId, docType, docId }); +} + +async function set(domainId, docType, docId, args) { + const res = await coll.findOneAndUpdate( + { domainId, docType, docId }, + { $set: { args } }, + { returnOriginal: false }, + ); + return res.value; +} + +function deleteOne(domainId, docType, docId) { + // TODO(twd2): delete status? + return coll.deleteOne({ domainId, docType, docId }); +} + +function deleteMulti(domainId, docType, args = {}) { + return coll.deleteMany({ ...args, domainId, docType }); +} + +function deleteMultiStatus(domainId, docType, args = {}) { + return collStatus.deleteMany({ ...args, domainId, docType }); +} + +function getMulti(domainId, docType, args = {}) { + return coll.find({ ...args, docType, domainId }); +} + +async function inc(domainId, docType, docId, key, value) { + const res = await coll.findOneAndUpdate( + { domainId, docType, docId }, + { $inc: { [key]: value } }, + { returnOriginal: false }, + ); + return res.value; +} + +async function incAndSet(domainId, docType, docId, key, value, args) { + const res = await coll.findOneAndUpdate( + { domainId, docType, docId }, + { $inc: { [key]: value }, $set: args }, + { returnOriginal: false }, + ); + return res.value; +} + +function count(domainId, docType, query) { + return coll.find({ ...query, docType, domainId }).count(); +} + +async function push(domainId, docType, docId, key, content, owner, args) { + const _id = new ObjectID(); + const doc = await coll.findOneAndUpdate( + { domainId, docType, docId }, + { + $push: { + [key]: { + ...args, content, owner, _id, + }, + }, + }, + { returnOriginal: false }, + ); + return [doc.value, _id]; +} + +async function pull(domainId, docType, docId, setKey, contents) { + const res = await coll.findOneAndUpdate( + { domainId, docType, docId }, + { $pull: { [setKey]: { $in: contents } } }, + { returnOriginal: false }, + ); + return res.value; +} + +async function deleteSub(domainId, docType, docId, key, subId) { + const res = await coll.findOneAndUpdate( + { domainId, docType, docId }, + { $pull: { [key]: { _id: subId } } }, + { returnOriginal: false }, + ); + return res.value; +} + +async function getSub(domainId, docType, docId, key, subId) { + const doc = await coll.findOne({ + domainId, + docType, + docId, + [key]: { $elemMatch: { _id: subId } }, + }); + if (!doc) return [null, null]; + for (const sdoc of doc[key] || []) { + if (sdoc._id === subId) { + return [doc, sdoc]; + } + } + return [doc, null]; +} + +async function setSub(domainId, docType, docId, key, subId, args) { + const $set = {}; + for (const k in args) { + $set[`${key}.$.${k}`] = args[k]; + } + const res = await coll.findOneAndUpdate( + { + domainId, + docType, + docId, + [key]: { $elemMatch: { _id: subId } }, + }, + { $set }, + { returnOriginal: false }, + ); + return res.value; +} + +async function addToSet(domainId, docType, docId, setKey, content) { + const res = await coll.findOneAndUpdate( + { domainId, docType, docId }, + { $addToSet: { [setKey]: content } }, + { returnOriginal: false }, + ); + return res.value; +} + +function getStatus(domainId, docType, docId, uid) { + return collStatus.findOne({ + domainId, docType, docId, uid, + }); +} + +function getMultiStatus(domainId, docType, args) { + return collStatus.find({ ...args, docType, domainId }); +} + +async function setStatus(domainId, docType, docId, uid, args) { + const res = await collStatus.findOneAndUpdate( + { + domainId, docType, docId, uid, + }, + { $set: args }, + { upsert: true, returnOriginal: false }, + ); + return res.value; +} + +async function setIfNotStatus(domainId, docType, docId, uid, key, value, ifNot, args) { + const res = await collStatus.findOneAndUpdate( + { + domainId, docType, docId, uid, key: { $not: { $eq: ifNot } }, + }, + { $set: { [key]: value, args } }, + { upsert: true, returnOriginal: false }, + ); + return res.value; +} + +async function cappedIncStatus( + domainId, docType, docId, uid, + key, value, minValue = -1, maxValue = 1, +) { + assert(value !== 0); + const $not = value > 0 ? { $gte: maxValue } : { $lte: minValue }; + const res = await collStatus.findOneAndUpdate( + { + domainId, docType, docId, uid, [key]: { $not }, + }, + { $inc: { key: value } }, + { upsert: true, returnOriginal: false }, + ); + return res.value; +} + +async function incStatus(domainId, docType, docId, uid, key, value) { + const res = await collStatus.findOneAndUpdate( + { + domainId, docType, docId, uid, + }, + { $inc: { [key]: value } }, + { upsert: true, returnOriginal: false }, + ); + return res.value; +} + +async function revPushStatus(domainId, docType, docId, uid, key, value) { + const res = await collStatus.findOneAndUpdate( + { + domainId, docType, docId, uid, + }, + { $push: { [key]: value }, $inc: { rev: 1 } }, + { upsert: true, returnOriginal: false }, + ); + return res.value; +} + +async function revInitStatus(domainId, docType, docId, uid) { + const res = await collStatus.findOneAndUpdate( + { + domainId, docType, docId, uid, + }, + { $inc: { rev: 1 } }, + { upsert: true, returnOriginal: false }, + ); + return res.value; +} + +async function revSetStatus(domainId, docType, docId, uid, rev, args, returnDoc = true) { + const filter = { + domainId, docType, docId, uid, rev, + }; + const update = { $set: args, $inc: { rev: 1 } }; + if (returnDoc) { + const res = await coll.findOneAndUpdate(filter, update, { returnOriginal: false }); + return res.value; + } + const res = await coll.updateOne(filter, update); + return res; +} + +async function ensureIndexes() { + await coll.createIndex({ domainId: 1, docType: 1, docId: 1 }, { unique: true }); + await coll.createIndex({ + domainId: 1, docType: 1, owner: 1, docId: -1, + }); + // For problem + await coll.createIndex({ + domainId: 1, docType: 1, category: 1, docId: 1, + }, { sparse: true }); + await coll.createIndex({ + domainId: 1, docType: 1, hidden: 1, category: 1, docId: 1, + }, { sparse: true }); + await coll.createIndex({ + domainId: 1, docType: 1, tag: 1, docId: 1, + }, { sparse: true }); + await coll.createIndex({ + domainId: 1, docType: 1, hidden: 1, tag: 1, docId: 1, + }, { sparse: true }); + // For problem solution + await coll.createIndex({ + domainId: 1, docType: 1, parentType: 1, parentId: 1, vote: -1, docId: -1, + }, { sparse: true }); + // For discussion + await coll.createIndex({ + domainId: 1, docType: 1, updateAt: -1, docId: -1, + }, { sparse: true }); + await coll.createIndex({ + domainId: 1, docType: 1, parentType: 1, parentId: 1, updateAt: -1, docId: -1, + }, { sparse: true }); + // Hidden doc + await coll.createIndex({ + domainId: 1, docType: 1, hidden: 1, docId: -1, + }, { sparse: true }); + // For contest + await coll.createIndex({ domainId: 1, docType: 1, pids: 1 }, { sparse: true }); + await coll.createIndex({ + domainId: 1, docType: 1, rule: 1, docId: -1, + }, { sparse: true }); + // For training + await coll.createIndex({ domainId: 1, docType: 1, 'dag.pids': 1 }, { sparse: true }); + + await collStatus.createIndex({ + domainId: 1, docType: 1, uid: 1, docId: 1, + }, { unique: true }); + // For rp system + await collStatus.createIndex({ + domainId: 1, docType: 1, docId: 1, status: 1, rid: 1, rp: 1, + }, { sparse: true }); + // For contest rule OI + await collStatus.createIndex({ + domainId: 1, docType: 1, docId: 1, score: -1, + }, { sparse: true }); + // For contest rule ACM + await collStatus.createIndex({ + domainId: 1, docType: 1, docId: 1, accept: -1, time: 1, + }, { sparse: true }); + // For training + await collStatus.createIndex({ + domainId: 1, docType: 1, uid: 1, enroll: 1, docId: 1, + }, { sparse: true }); +} + +global.Hydro.model.document = module.exports = { + add, + addToSet, + cappedIncStatus, + count, + deleteMulti, + deleteMultiStatus, + deleteOne, + deleteSub, + ensureIndexes, + get, + getMulti, + getMultiStatus, + getStatus, + getSub, + inc, + incAndSet, + incStatus, + pull, + push, + revInitStatus, + revPushStatus, + revSetStatus, + set, + setIfNotStatus, + setStatus, + setSub, + + TYPE_DOMAIN_ROLE, + TYPE_CONTEST, + TYPE_DISCUSSION, + TYPE_DISCUSSION_NODE, + TYPE_DISCUSSION_REPLY, + TYPE_HOMEWORK, + TYPE_PROBLEM, + TYPE_PROBLEM_LIST, + TYPE_PROBLEM_SOLUTION, + TYPE_FILE, + TYPE_TRAINING, +}; diff --git a/hydro/model/domain.js b/hydro/model/domain.js new file mode 100644 index 000000000..a106c206b --- /dev/null +++ b/hydro/model/domain.js @@ -0,0 +1,96 @@ +const builtin = require('./builtin'); +const document = require('./document'); +const db = require('../service/db'); +const validator = require('../lib/validator'); + +const coll = db.collection('domain'); + +/** + * @typedef {import('../interface').Pdoc} Pdoc + * @typedef {import('bson').ObjectID} ObjectID + * @typedef {import('mongodb').Cursor} Cursor + */ + +/** + * @param {string} domainId + * @param {number} owner + */ +function add(domainId, owner) { + const tasks = [ + coll.insertOne({ _id: domainId, owner, bulletin: '' }), + ]; + for (const id in builtin.BUILTIN_ROLES) { + tasks.push( + document.add( + domainId, builtin.BUILTIN_ROLES[id].perm, owner, + document.TYPE_DOMAIN_ROLE, id, + ), + ); + } + return Promise.all(tasks); +} + +/** + * @param {string} domainId + * @returns {Ddoc} + */ +async function get(domainId) { + return coll.findOne({ _id: domainId }); +} + +/** + * @param {object} query + * @param {object} sort + * @param {number} page + * @param {number} limit + * @returns {Ddoc[]} + */ +function getMany(query, sort, page, limit) { + return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit) + .toArray(); +} + +/** + * @param {object} query + * @returns {Cursor} + */ +function getMulti(query) { + return coll.find(query); +} + +/** + * @param {string} domainId + * @param {object} $set + * @returns {Ddoc} + */ +function edit(domainId, $set) { + if ($set.title) validator.checkTitle($set.title); + if ($set.content) validator.checkContent($set.content); + return coll.updateOne({ _id: domainId }, { $set }); +} + +async function inc(domainId, field, n) { + const res = await coll.findOneAndUpdate( + { _id: domainId }, + { $inc: { [field]: n } }, + { returnOriginal: false }, + ); + return res.value; +} + +async function getList(domainIds) { + const r = {}; + // eslint-disable-next-line no-await-in-loop + for (const domainId of domainIds) r[domainId] = await get(domainId); + return r; +} + +global.Hydro.model.domain = module.exports = { + add, + inc, + get, + getMany, + edit, + getMulti, + getList, +}; diff --git a/hydro/model/index.js b/hydro/model/index.js index 03ea6a372..822237a68 100644 --- a/hydro/model/index.js +++ b/hydro/model/index.js @@ -2,6 +2,7 @@ module.exports = { blacklist: require('./blacklist'), builtin: require('./builtin'), contest: require('./contest'), + document: require('./document'), opcount: require('./opcount'), problem: require('./problem'), record: require('./record'), diff --git a/hydro/model/message.js b/hydro/model/message.js index 827243959..21c65a38e 100644 --- a/hydro/model/message.js +++ b/hydro/model/message.js @@ -55,7 +55,7 @@ function getMulti(uid) { return coll.find({ $or: [{ from: uid }, { to: uid }] }); } -function index() { +function ensureIndexes() { return Promise.all([ coll.createIndex({ to: 1, _id: -1 }), coll.createIndex({ from: 1, _id: -1 }), @@ -69,5 +69,5 @@ global.Hydro.model.message = module.exports = { getMany, getMulti, send, - index, + ensureIndexes, }; diff --git a/hydro/model/problem.js b/hydro/model/problem.js index cf372c5d6..0f3cf210d 100644 --- a/hydro/model/problem.js +++ b/hydro/model/problem.js @@ -1,12 +1,10 @@ const { ObjectID } = require('bson'); const { STATUS_ACCEPTED } = require('./builtin').STATUS; const file = require('./file'); +const document = require('./document'); +const domain = require('./domain'); const { ProblemNotFoundError } = require('../error'); const validator = require('../lib/validator'); -const db = require('../service/db'); - -const coll = db.collection('problem'); -const collStatus = db.collection('problem.status'); /** * @typedef {import('../interface').Pdoc} Pdoc @@ -15,6 +13,7 @@ const collStatus = db.collection('problem.status'); */ /** + * @param {string} domainId * @param {string} title * @param {string} content * @param {number} owner @@ -24,10 +23,7 @@ const collStatus = db.collection('problem.status'); * @param {string[]} tag * @param {boolean} hidden */ -async function add({ - title, - content, - owner, +async function add(domainId, title, content, owner, { pid = null, data = null, category = [], @@ -36,134 +32,118 @@ async function add({ }) { validator.checkTitle(title); validator.checkContent(content); - const res = await coll.insertOne({ - content, - owner, - pid, - title, - data, - category, - tag, - hidden, - nSubmit: 0, - nAccept: 0, + const d = await domain.inc(domainId, 'pidCounter', 1); + if (!pid) pid = d.pidCounter.toString(); + return await document.add(domainId, content, owner, document.TYPE_PROBLEM, d.pidCounter, null, null, { + pid, title, data, category, tag, hidden, nSubmit: 0, nAccept: 0, }); - return res.insertedId; } + /** - * @param {string|ObjectID} pid + * @param {string} domainId + * @param {string|number} pid * @param {number} uid * @returns {Pdoc} */ -async function get(pid, uid = null) { - let query = {}; - if (pid.generationTime || pid.length === 24) query = { _id: new ObjectID(pid) }; - else query = { pid }; - const pdoc = await coll.findOne(query); +async function get(domainId, pid, uid = null) { + const pdoc = await document.get(domainId, document.TYPE_PROBLEM, pid); if (!pdoc) throw new ProblemNotFoundError(pid); if (uid) { - query.uid = uid; - pdoc.psdoc = await collStatus.findOne(query); + pdoc.psdoc = await document.getStatus(domainId, document.TYPE_PROBLEM, pdoc.docId, uid); } return pdoc; } + /** - * @param {ObjectID} pid - * @returns {Pdoc} - */ -async function getById(_id) { - _id = new ObjectID(_id); - const pdoc = await coll.findOne({ _id }); - if (!pdoc) throw new ProblemNotFoundError(_id); - return pdoc; -} -/** - * @param {string|ObjectID} pid - * @param {number} uid + * @param {string} domainId + * @param {object} query + * @param {object} sort + * @param {number} page + * @param {number} limit * @returns {Pdoc[]} */ -function getMany(query, sort, page, limit) { - return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit) +function getMany(domainId, query, sort, page, limit) { + return document.getMulti(domainId, query).sort(sort).skip((page - 1) * limit).limit(limit) .toArray(); } + /** + * @param {string} domainId * @param {object} query * @returns {Cursor} */ -function getMulti(query) { - return coll.find(query); +function getMulti(domainId, query) { + return document.getMulti(domainId, document.TYPE_PROBLEM, query); } + /** + * @param {string} domainId * @param {object} query * @returns {Cursor} */ -function getMultiStatus(query) { - return collStatus.find(query); +function getMultiStatus(domainId, query) { + return document.getMultiStatus(domainId, document.TYPE_PROBLEM, query); } + /** + * @param {string} domainId * @param {ObjectID} _id * @param {object} query * @returns {Pdoc} */ -async function edit(_id, $set) { +function edit(domainId, _id, $set) { if ($set.title) validator.checkTitle($set.title); if ($set.content) validator.checkContent($set.content); - await coll.findOneAndUpdate({ _id }, { $set }); - const pdoc = await getById(_id); - if (!pdoc) throw new ProblemNotFoundError(_id); - return pdoc; + return document.set(domainId, document.TYPE_CONTEST, _id, $set); } -async function inc(_id, field, n) { - await coll.findOneAndUpdate({ _id }, { $inc: { [field]: n } }); - const pdoc = await getById(_id); - if (!pdoc) throw new ProblemNotFoundError(_id); - return pdoc; + +function inc(domainId, _id, field, n) { + return document.inc(domainId, document.TYPE_PROBLEM, _id, field, n); } -function count(query) { - return coll.find(query).count(); + +function count(domainId, query) { + return document.count(domainId, query); } -async function random(query) { - const cursor = coll.find(query); + +async function random(domainId, query) { + const cursor = document.getMulti(domainId, query); const pcount = await cursor.count(); if (pcount) { const pdoc = await cursor.skip(Math.floor(Math.random() * pcount)).limit(1).toArray(); return pdoc[0].pid; } return null; } -async function getList(pids) { + +async function getList(domainId, pids) { const r = {}; - for (const pid of pids) r[pid] = await get(pid); // eslint-disable-line no-await-in-loop + // eslint-disable-next-line no-await-in-loop + for (const pid of pids) r[pid] = await get(domainId, pid); return r; } -async function getListStatus(uid, pids) { - const psdocs = await getMultiStatus({ uid, pid: { $in: Array.from(new Set(pids)) } }).toArray(); - const r = {}; - for (const psdoc of psdocs) r[psdoc.pid] = psdoc; - return r; -} -async function updateStatus(pid, uid, rid, status) { - const cur = collStatus.findOne({ pid, uid }); - if (cur && status !== STATUS_ACCEPTED) { - if (cur.status !== STATUS_ACCEPTED) return false; - } - await collStatus.updateOne( - { pid, uid }, - { $set: { rid, status } }, - { upsert: true }, - ); - return true; -} -async function setTestdata(_id, readStream) { - const pdoc = await getById(_id); - const id = await file.add(readStream, 'data.zip'); - if (pdoc.data && typeof pdoc.data === 'object') file.dec(this.pdoc.data); - return await edit(_id, { data: id }); // eslint-disable-line no-return-await -} -async function getData(pid) { - const pdoc = await get(pid); - if (!pdoc.data) return null; - return file.get(pdoc.data); +async function getListStatus(domainId, uid, pids) { + const psdocs = await getMultiStatus( + domainId, { uid, docId: { $in: Array.from(new Set(pids)) } }, + ).toArray(); + const r = {}; + for (const psdoc of psdocs) r[psdoc.docId] = psdoc; + return r; +} + +async function updateStatus(domainId, pid, uid, rid, status) { + try { + await document.setIfNotStatus(domainId, document.TYPE_PROBLEM, pid, uid, 'status', status, STATUS_ACCEPTED, { rid }); + } catch (e) { + return false; + } + return true; +} + +async function setTestdata(domainId, _id, readStream) { + const pdoc = await get(domainId, _id); + const id = await file.add(readStream, 'data.zip'); + if (pdoc.data && typeof pdoc.data === 'object') file.dec(this.pdoc.data); + return await edit(domainId, _id, { data: id }); // eslint-disable-line no-return-await } global.Hydro.model.problem = module.exports = { @@ -174,12 +154,10 @@ global.Hydro.model.problem = module.exports = { edit, count, random, - getById, getMulti, getList, getListStatus, getMultiStatus, setTestdata, updateStatus, - getData, }; diff --git a/hydro/model/record.js b/hydro/model/record.js index f60a23be9..105405e5e 100644 --- a/hydro/model/record.js +++ b/hydro/model/record.js @@ -9,11 +9,13 @@ const db = require('../service/db'); const coll = db.collection('record'); /** + * @param {string} domainId * @param {import('../interface').Record} data */ -async function add(data) { +async function add(domainId, data) { _.defaults(data, { status: STATUS_WAITING, + domainId, score: 0, time: 0, memory: 0, @@ -27,32 +29,37 @@ async function add(data) { const res = await coll.insertOne(data); return res.insertedId; } + /** - * @param {string} rid + * @param {string} domainId + * @param {ObjectID} rid * @returns {import('../interface').Record} */ -async function get(rid) { +async function get(domainId, rid) { const _id = new ObjectID(rid); - const rdoc = await coll.findOne({ _id }); + const rdoc = await coll.findOne({ domainId, _id }); if (!rdoc) throw new RecordNotFoundError(rid); return rdoc; } -function getMany(query, sort, page, limit) { - return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit) + +function getMany(domainId, query, sort, page, limit) { + return coll.find({ ...query, domainId }).sort(sort).skip((page - 1) * limit).limit(limit) .toArray(); } -async function update(rid, $set, $push) { + +async function update(domainId, rid, $set, $push) { const _id = new ObjectID(rid); const upd = {}; if ($set && Object.keys($set).length) upd.$set = $set; if ($push && Object.keys($push).length) upd.$push = $push; - await coll.findOneAndUpdate({ _id }, upd); + await coll.findOneAndUpdate({ domainId, _id }, upd); const rdoc = await coll.findOne({ _id }); if (!rdoc) throw new RecordNotFoundError(rid); return rdoc; } -function reset(rid) { - return update(rid, { + +function reset(domainId, rid) { + return update(domainId, rid, { score: 0, status: STATUS_WAITING, time: 0, @@ -65,24 +72,29 @@ function reset(rid) { rejudged: true, }); } -function count(query) { - return coll.find(query).count(); -} -async function getList(rids) { - const r = {}; - for (const rid of rids) r[rid] = await get(rid); // eslint-disable-line no-await-in-loop - return r; -} -function getUserInProblemMulti(uid, pid) { - return coll.find({ owner: uid, pid }); + +function count(domainId, query) { + return coll.find({ domainId, ...query }).count(); } -async function judge(rid) { - const rdoc = await get(rid); - const pdoc = await problem.getById(rdoc.pid); +async function getList(domainId, rids) { + const r = {}; + // eslint-disable-next-line no-await-in-loop + for (const rid of rids) r[rid] = await get(domainId, rid); + return r; +} + +function getUserInProblemMulti(domainId, uid, pid) { + return coll.find({ domainId, owner: uid, pid }); +} + +async function judge(domainId, rid) { + const rdoc = await get(domainId, rid); + const pdoc = await problem.get(domainId, rdoc.pid); await task.add({ type: 'judge', rid, + domainId, pid: rdoc.pid, data: pdoc.data, lang: rdoc.lang, @@ -90,13 +102,14 @@ async function judge(rid) { }); } -async function rejudge(rid) { - await reset(rid); - const rdoc = await get(rid); - const pdoc = await problem.getById(rdoc.pid); +async function rejudge(domainId, rid) { + await reset(domainId, rid); + const rdoc = await get(domainId, rid); + const pdoc = await problem.get(domainId, rdoc.pid); await task.add({ type: 'judge', rid, + domainId, pid: rdoc.pid, data: pdoc.data, lang: rdoc.lang, diff --git a/hydro/model/solution.js b/hydro/model/solution.js index 5182b0cc2..0ce92c4de 100644 --- a/hydro/model/solution.js +++ b/hydro/model/solution.js @@ -1,107 +1,80 @@ -const { ObjectID } = require('bson'); +const document = require('./document'); const { SolutionNotFoundError } = require('../error'); -const validator = require('../lib/validator'); -const db = require('../service/db'); - -const coll = db.collection('solution'); -const collStatus = db.collection('solution.status'); /** * @param {string} pid * @param {number} owner * @param {string} content */ -async function add(pid, owner, content) { - validator.checkContent(content); - pid = new ObjectID(pid); - const res = await coll.insertOne({ - content, owner, pid, reply: [], vote: 0, - }); - return res.insertedId; +function add(domainId, pid, owner, content) { + return document.add( + domainId, content, owner, document.TYPE_PROBLEM_SOLUTION, + null, document.TYPE_PROBLEM, pid, { reply: [], vote: 0 }, + ); } -async function get(psid) { - psid = new ObjectID(psid); - const psdoc = await coll.findOne({ _id: psid }); +async function get(domainId, psid) { + const psdoc = await document.get(domainId, document.TYPE_PROBLEM_SOLUTION, psid); if (!psdoc) throw new SolutionNotFoundError(); return psdoc; } -function getMany(query, sort, page, limit) { - return coll.find(query).sort(sort) +function getMany(domainId, query, sort, page, limit) { + return document.getMulti(domainId, document.TYPE_PROBLEM_SOLUTION, query) + .sort(sort) .skip((page - 1) * limit).limit(limit) .toArray(); } -async function edit(_id, content) { - validator.checkContent(content); - await coll.updateOne({ _id }, { $set: { content } }); - const psdoc = await get(_id); - if (!psdoc) throw new SolutionNotFoundError(_id); - return psdoc; +function edit(domainId, psid, content) { + return document.set(domainId, document.TYPE_PROBLEM_SOLUTION, psid, { content }); } -function del(psid) { - return coll.deleteOne({ _id: psid }); +function del(domainId, psid) { + return document.deleteOne(domainId, document.TYPE_PROBLEM_SOLUTION, psid); } -function count(query) { - return coll.find(query).count(); +function count(domainId, query) { + return document.count(domainId, document.TYPE_PROBLEM_SOLUTION, query); } -function getMulti(pid) { - return coll.find({ pid }).sort({ vote: -1 }); +function getMulti(domainId, pid) { + return document.getMulti( + domainId, document.TYPE_PROBLEM_SOLUTION, + { parentType: document.TYPE_PROBLEM, parentId: pid }, + ).sort({ vote: -1 }); } -function reply(psid, owner, content) { - validator.checkContent(content); - return coll.findOneAndUpdate( - { _id: psid }, - { $push: { reply: { content, owner, _id: new ObjectID() } } }, - ); +function reply(domainId, psid, owner, content) { + return document.push(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', content, owner); } -async function getReply(psid, psrid) { - const psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } }); - if (!psdoc) return [null, null]; - for (const psrdoc of psdoc) if (psrdoc._id === psrid) return [psdoc, psrdoc]; - return [psdoc, null]; +function getReply(domainId, psid, psrid) { + return document.getSub(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', psrid); } -async function editReply(psid, psrid, content) { - validator.checkContent(content); - psid = new ObjectID(psid); - psrid = new ObjectID(psrid); - const psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } }); - const { reply } = psdoc; // eslint-disable-line no-shadow - for (const i in reply) { - if (reply[i]._id === psrid) { - reply[i].content = content; - break; - } - } - // eslint-disable-next-line no-return-await - return await coll.updateOne({ _id: psdoc._id }, { $set: { reply } }); +function editReply(domainId, psid, psrid, content) { + return document.setSub(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', psrid, { content }); } -function delReply(psid, psrid) { - return coll.findOneAndUpdate({ _id: psid }, { $pull: { reply: { _id: psrid } } }); +function delReply(domainId, psid, psrid) { + return document.deleteSub(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'reply', psrid); } -async function vote(psid, uid, value) { - let pssdoc = await collStatus.findOne({ psid, uid }); - if (pssdoc) await collStatus.deleteOne({ psid, uid }); - await collStatus.insertOne({ psid, uid, vote: value }); +async function vote(domainId, psid, uid, value) { + let pssdoc = await document.getStatus(domainId, document.TYPE_PROBLEM_SOLUTION, psid, uid); + await document.setStatus(domainId, document.TYPE_PROBLEM_SOLUTION, psid, uid, { vote: value }); if (pssdoc) value += -pssdoc.vote; - await coll.updateOne({ _id: psid }, { $inc: { vote: value } }); - pssdoc = await collStatus.findOne({ psid, uid }); - const psdoc = await coll.findOne({ _id: psid }); + const psdoc = await document.inc(domainId, document.TYPE_PROBLEM_SOLUTION, psid, 'vote', value); + pssdoc = await document.getStatus(domainId, document.TYPE_PROBLEM_SOLUTION, psid, uid); return [psdoc, pssdoc]; } -async function getListStatus(list, uid) { +async function getListStatus(domainId, list, uid) { const result = {}; - const res = await collStatus.find({ uid, psid: { $in: list } }).toArray(); + const res = await document.getMultiStatus( + domainId, document.TYPE_PROBLEM_SOLUTION, { uid, psid: { $in: list } }, + ).toArray(); for (const i of res) result[i.psid] = i; return result; } diff --git a/hydro/model/training.js b/hydro/model/training.js index ce39d5864..f1184dd02 100644 --- a/hydro/model/training.js +++ b/hydro/model/training.js @@ -1,38 +1,67 @@ const assert = require('assert'); +const document = require('./document'); const validator = require('../lib/validator'); const { ValidationError, TrainingNotFoundError, TrainingAlreadyEnrollError } = require('../error'); -const db = require('../service/db'); -const coll = db.collection('traning'); -const collStatus = db.collection('training.status'); - -function getStatus(tid, uid) { - return collStatus.findOne({ tid, uid }); +function getStatus(domainId, tid, uid) { + return document.getStatus(domainId, document.TYPE_TRAINING, tid, uid); } -function getMultiStatus(query) { - return collStatus.find(query); +function getMultiStatus(domainId, query) { + return document.getMultiStatus(domainId, document.TYPE_TRAINING, query); } -async function getListStatus(uid, tids) { - const tsdocs = await getMultiStatus({ uid, tid: { $in: Array.from(new Set(tids)) } }).toArray(); +async function getListStatus(domainId, uid, tids) { + const tsdocs = await getMultiStatus( + domainId, { uid, tid: { $in: Array.from(new Set(tids)) } }, + ).toArray(); const r = {}; for (const tsdoc of tsdocs) r[tsdoc.pid] = tsdoc; return r; } -async function enroll(tid, uid) { +async function enroll(domainId, tid, uid) { try { - await collStatus.insertOne({ tid, uid, enroll: 1 }); + await document.setStatus(domainId, document.TYPE_TRAINING, tid, uid, { enroll: 1 }); } catch (e) { throw new TrainingAlreadyEnrollError(tid, uid); } - await coll.updateOne({ _id: tid }, { $inc: { enroll: 1 } }); + return await document.inc(domainId, document.TYPE_TRAINING, tid, 'enroll', 1); } -async function setStatus(tid, uid, $set) { - await collStatus.updateOne({ tid, uid }, { $set }); - return await collStatus.findOne({ tid, uid }); // eslint-disable-line no-return-await +function setStatus(domainId, tid, uid, $set) { + return document.setStatus(domainId, document.TYPE_TRAINING, tid, uid, $set); +} + +function add(domainId, title, content, owner, dag = [], description = '') { + validator.checkTitle(title); + validator.checkIntro(content); + validator.checkDescription(description); + for (const node of dag) { + for (const nid of node.requireNids) { + if (nid >= node._id) throw new ValidationError('dag'); + } + } + return document.add(domainId, content, owner, document.TYPE_TRAINING, null, null, null, { + dag, + title, + description, + enroll: 0, + }); +} + +function edit(domainId, tid, $set) { + if ($set.title) validator.checkTitle($set.title); + if ($set.content) validator.checkIntro($set.content); + if ($set.desc) validator.checkDescription($set.description); + if ($set.dag) { + for (const node of $set.dag) { + for (const nid of node.requireNids) { + assert(nid >= node._id, new ValidationError('dag')); + } + } + } + return document.set(domainId, document.TYPE_TRAINING, tid, $set); } global.Hydro.model.training = module.exports = { @@ -66,54 +95,23 @@ global.Hydro.model.training = module.exports = { isInvalid(node, doneNids) { return (!Set.isSuperset(new Set(doneNids), new Set(node.requireNids))); }, - async add(title, content, owner, dag = [], description = '') { - validator.checkTitle(title); - validator.checkIntro(content); - validator.checkDescription(description); - for (const node of dag) { - for (const nid of node.requireNids) { - if (nid >= node._id) throw new ValidationError('dag'); - } - } - const res = await coll.insertOne({ - content, - owner, - dag, - title, - description, - enroll: 0, - }); - return res.insertedId; - }, - count: (query) => coll.find(query).count(), - async edit(tid, $set) { - if ($set.title) validator.checkTitle($set.title); - if ($set.content) validator.checkIntro($set.content); - if ($set.desc) validator.checkDescription($set.description); - if ($set.dag) { - for (const node of $set.dag) { - for (const nid of node.requireNids) { - assert(nid >= node._id, new ValidationError('dag')); - } - } - } - await coll.findOneAndUpdate({ _id: tid }, { $set }); - const tdoc = await coll.findOne({ _id: tid }); + add, + edit, + count: (domainId, query) => document.count(domainId, document.TYPE_TRAINING, query), + async get(domainId, tid) { + const tdoc = await document.get(domainId, document.TYPE_TRAINING, tid); if (!tdoc) throw new TrainingNotFoundError(tid); return tdoc; }, - async get(tid) { - const tdoc = await coll.findOne({ _id: tid }); - if (!tdoc) throw new TrainingNotFoundError(tid); - return tdoc; - }, - async getList(tids) { - const tdocs = await this.getMulti({ _id: { $in: Array.from(new Set(tids)) } }).toArray(); + async getList(domainId, tids) { + const tdocs = await this.getMulti( + domainId, { _id: { $in: Array.from(new Set(tids)) } }, + ).toArray(); const r = {}; - for (const tdoc of tdocs) r[tdoc._id] = tdoc; + for (const tdoc of tdocs) r[tdoc.docId] = tdoc; return r; }, - getMulti: (query) => coll.find(query), + getMulti: (domainId, query) => document.getMulti(domainId, document.TYPE_TRAINING, query), getMultiStatus, getStatus, enroll, diff --git a/hydro/model/user.js b/hydro/model/user.js index 962050825..edf19dc83 100644 --- a/hydro/model/user.js +++ b/hydro/model/user.js @@ -1,3 +1,4 @@ +const document = require('./document'); const system = require('./system'); const { UserNotFoundError, UserAlreadyExistError } = require('../error'); const perm = require('../permission'); @@ -5,7 +6,6 @@ const pwhash = require('../lib/hash.hydro'); const db = require('../service/db'); const coll = db.collection('user'); -const collRole = db.collection('role'); class USER { constructor(user) { @@ -37,38 +37,46 @@ class USER { } } -async function getById(_id) { +async function getPerm(domainId, udoc) { + if (udoc.priv === 1) { + const p = await document.get(domainId, document.TYPE_DOMAIN_ROLE, 'admin'); + return p.content; + } + const role = await document.getStatus(domainId, document.TYPE_DOMAIN_ROLE, 0, udoc._id); + const p = await document.get(domainId, document.TYPE_DOMAIN_ROLE, role || 'default'); + return p.content; +} + +async function getById(domainId, _id) { const udoc = await coll.findOne({ _id }); if (!udoc) throw new UserNotFoundError(_id); - const role = await collRole.findOne({ _id: udoc.role || 'default' }); - udoc.perm = role.perm; + udoc.perm = await getPerm(domainId, udoc); return new USER(udoc); } -async function getList(uids) { +async function getList(domainId, uids) { const r = {}; - for (const uid of uids) r[uid] = await getById(uid); // eslint-disable-line no-await-in-loop + // eslint-disable-next-line no-await-in-loop + for (const uid of uids) r[uid] = await getById(domainId, uid); return r; } -async function getByUname(uname) { +async function getByUname(domainId, uname) { const unameLower = uname.trim().toLowerCase(); const udoc = await coll.findOne({ unameLower }); if (!udoc) throw new UserNotFoundError(uname); - const role = await collRole.findOne({ _id: udoc.role || 'default' }); - udoc.perm = role.perm; + udoc.perm = await getPerm(domainId, udoc); return new USER(udoc); } -async function getByEmail(mail, ignoreMissing = false) { +async function getByEmail(domainId, mail, ignoreMissing = false) { const mailLower = mail.trim().toLowerCase(); const udoc = await coll.findOne({ mailLower }); if (!udoc) { if (ignoreMissing) return null; throw new UserNotFoundError(mail); } - const role = await collRole.findOne({ _id: udoc.role || 'default' }); - udoc.perm = role.perm; + udoc.perm = await getPerm(domainId, udoc); return new USER(udoc); } @@ -106,30 +114,27 @@ async function inc(_id, field, n = 1) { } async function create({ - uid, mail, uname, password, regip = '127.0.0.1', role = 'default', + uid, mail, uname, password, regip = '127.0.0.1', priv = perm.PRIV_NONE, }) { const salt = String.random(); if (!uid) uid = system.inc('user'); try { - await Promise.all([ - coll.insertOne({ - _id: uid, - mail, - mailLower: mail.trim().toLowerCase(), - uname, - unameLower: uname.trim().toLowerCase(), - password: pwhash.hash(password, salt), - salt, - hashType: 'hydro', - regat: new Date(), - regip, - loginat: new Date(), - loginip: regip, - role, - gravatar: mail, - }), - collRole.updateOne({ _id: role }, { $inc: { count: 1 } }), - ]); + await coll.insertOne({ + _id: uid, + mail, + mailLower: mail.trim().toLowerCase(), + uname, + unameLower: uname.trim().toLowerCase(), + password: pwhash.hash(password, salt), + salt, + hashType: 'hydro', + regat: new Date(), + regip, + loginat: new Date(), + loginip: regip, + priv, + gravatar: mail, + }); } catch (e) { throw new UserAlreadyExistError([uid, uname, mail]); } @@ -146,37 +151,31 @@ async function getPrefixList(prefix, limit = 50) { return udocs; } -async function setRole(uid, role) { - const udoc = await getById(uid); - return await Promise.all([ // eslint-disable-line no-return-await - coll.findOneAndUpdate({ _id: uid }, { $set: { role } }), - collRole.updateOne({ _id: udoc.role }, { $inc: { count: -1 } }), - collRole.updateOne({ _id: role }, { $inc: { count: 1 } }), +function setRole(domainId, uid, role) { + return document.setStatus(domainId, document.TYPE_DOMAIN_ROLE, 0, uid, { role }); +} + +function getRoles(domainId) { + return document.getMulti(domainId, document.TYPE_DOMAIN_ROLE).sort('_id', 1).toArray(); +} + +function getRole(domainId, name) { + return document.get(domainId, document.TYPE_DOMAIN_ROLE, name); +} + +function addRole(domainId, name, permission) { + return document.add(domainId, permission, 1, document.TYPE_DOMAIN_ROLE, name); +} + +function deleteRoles(domainId, roles) { + return Promise.all([ + document.deleteMulti(domainId, document.TYPE_DOMAIN_ROLE, { docId: { $in: roles } }), + document.deleteMultiStatus(domainId, document.TYPE_DOMAIN_ROLE, { role: { $in: roles } }), ]); } -function getRoles() { - return collRole.find().sort('_id', 1).toArray(); -} - -function getRole(name) { - return collRole.findOne({ _id: name }); -} - -function addRole(name, permission) { - return collRole.insertOne({ _id: name, perm: permission, count: 0 }); -} - -function deleteRoles(roles) { +function ensureIndexes() { return Promise.all([ - coll.updateMany({ role: { $in: roles } }, { $set: { role: 'default' } }), - collRole.deleteMany({ _id: { $in: roles } }), - ]); -} - -function index() { - return Promise.all([ - collRole.updateOne({ _id: 'root' }, { $set: { perm: perm.PERM_ALL } }), coll.createIndex('unameLower', { unique: true }), coll.createIndex('mailLower', { sparse: true }), ]); @@ -200,5 +199,5 @@ global.Hydro.model.user = module.exports = { getRoles, addRole, deleteRoles, - index, + ensureIndexes, }; diff --git a/hydro/permission.js b/hydro/permission.js index 82965b2b3..e7fc93531 100644 --- a/hydro/permission.js +++ b/hydro/permission.js @@ -114,4 +114,7 @@ perm.PERM_DEFAULT = perm.PERM_BASIC perm.PERM_ADMIN = perm.PERM_ALL; +perm.PRIV_NONE = 0; +perm.PRIV_ADMIN = 1; + global.Hydro.permission = module.exports = perm; diff --git a/hydro/script/install.js b/hydro/script/install.js index 2a21b86f8..72ecbe473 100644 --- a/hydro/script/install.js +++ b/hydro/script/install.js @@ -3,11 +3,22 @@ const db = require('../service/db'); const builtin = require('../model/builtin'); const discussion = require('../model/discussion'); const system = require('../model/system'); +const domain = require('../model/domain'); +const user = require('../model/user'); const pwhash = require('../lib/hash.hydro'); const { udoc } = require('../interface'); const collUser = db.collection('user'); -const collRole = db.collection('role'); + +const allowFail = async (func, ...args) => { + let res; + try { + res = await func(...args); + } catch (e) { + return null; + } + return res; +}; async function run() { const def = { @@ -28,7 +39,6 @@ async function run() { 'smtp.pass': '', 'smtp.secure': false, 'db.ver': 1, - 'listen.https': false, 'listen.port': 8888, 'session.keys': ['Hydro'], 'session.secure': false, @@ -42,26 +52,30 @@ async function run() { tasks.push(system.set(key, def[key])); } await Promise.all(tasks); - try { - const salt = String.random(); - await collUser.insertMany([ - defaults({ + const salt = String.random(); + tasks = [ + allowFail(domain.add, 'system', 1), + user.setRole('system', 0, 'guest'), + collUser.updateOne({ _id: 0 }, { + $set: defaults({ _id: 0, uname: 'Hydro', unameLower: 'hydro', mail: 'hydro@hydro', mailLower: 'hydro@hydro', - role: 'guest', }, udoc), - defaults({ + }, { upsert: true }), + collUser.updateOne({ _id: 1 }, { + $set: defaults({ _id: 1, mail: 'guest@hydro', mailLower: 'guest@hydro', uname: 'Guest', unameLower: 'guest', - role: 'guest', }, udoc), - defaults({ + }, { upsert: true }), + collUser.updateOne({ _id: -1 }, { + $set: defaults({ _id: -1, mail: 'root@hydro', mailLower: 'root@hydro', @@ -70,15 +84,10 @@ async function run() { hash: pwhash.hash('rootroot', salt), salt, gravatar: 'root@hydro', - role: 'root', + priv: 1, }, udoc), - ]); - } catch (e) { - // Ignore user already exist error - } - await collRole.deleteMany({ _id: { $in: builtin.BUILTIN_ROLES.map((role) => role._id) } }); - await collRole.insertMany(builtin.BUILTIN_ROLES); - tasks = []; + }, { upsert: true }), + ]; for (const category in builtin.DEFAULT_NODES) { const nodes = builtin.DEFAULT_NODES[category]; for (const node of nodes) { diff --git a/hydro/service/server.js b/hydro/service/server.js index c9a63c741..cde098af0 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -8,7 +8,6 @@ const Router = require('koa-router'); const cache = require('koa-static-cache'); const sockjs = require('sockjs'); const http = require('http'); -const https = require('https'); const validator = require('../lib/validator'); const template = require('../lib/template'); const user = require('../model/user'); @@ -26,8 +25,7 @@ let server; const router = new Router(); async function prepare() { - const useHttps = await system.get('listen.https'); - server = (useHttps ? https : http).createServer(app.callback()); + server = http.createServer(app.callback()); app.keys = await system.get('session.keys'); app.use(cache(path.join(os.tmpdir(), 'hydro', 'builtin'), { maxAge: 365 * 24 * 60 * 60, @@ -129,7 +127,7 @@ class Handler { this.response.disposition = `attachment; filename="${name}"`; } - async ___prepare() { + async ___prepare({ domainId }) { this.response.body = {}; this.now = new Date(); this._handler.sid = this.request.cookies.get('sid'); @@ -149,7 +147,7 @@ class Handler { if (!this.session) this.session = { uid: 1 }; const bdoc = await blacklist.get(this.request.ip); if (bdoc) throw new BlacklistedError(this.request.ip); - this.user = await user.getById(this.session.uid); + this.user = await user.getById(domainId, this.session.uid); if (!this.user) throw new UserNotFoundError(this.session.uid); [this.csrfToken] = await token.add(token.TYPE_CSRF_TOKEN, 600, { path: this.request.path, @@ -249,7 +247,10 @@ function Route(route, RouteHandler) { const h = new RouteHandler(ctx); try { const method = ctx.method.toLowerCase(); - const args = { ...ctx.params, ...ctx.query, ...ctx.request.body }; + // TODO(masnn) domainId + const args = { + ...ctx.params, ...ctx.query, ...ctx.request.body, domainId: 'system', + }; if (h.___prepare) await h.___prepare(args); try { @@ -259,6 +260,9 @@ function Route(route, RouteHandler) { if (!args[l]) throw new ValidationError(l); } } + if (args.pid) { + if (Number.isInteger(Number(args.pid))) args.pid = Number(args.pid); + } if (args.content) validator.checkContent(args.content); if (args.title) validator.checkContent(args.title); if (args.uid) args.uid = parseInt(validator.checkUid(args.uid)); @@ -335,7 +339,7 @@ class ConnectionHandler { this.conn.close(code, reason); } - async ___prepare() { + async ___prepare({ domainId }) { try { this.session = await token.get(this.request.params.token, token.TYPE_CSRF_TOKEN); await token.delete(this.request.params.token, token.TYPE_CSRF_TOKEN); @@ -344,7 +348,7 @@ class ConnectionHandler { } const bdoc = await blacklist.get(this.request.ip); if (bdoc) throw new BlacklistedError(this.request.ip); - this.user = await user.getById(this.session.uid); + this.user = await user.getById(domainId, this.session.uid); if (!this.user) throw new UserNotFoundError(this.session.uid); } } @@ -354,7 +358,7 @@ function Connection(prefix, RouteConnHandler) { sock.on('connection', async (conn) => { const h = new RouteConnHandler(conn); try { - const args = { ...h.request.params }; + const args = { ...h.request.params, domainId: 'system' }; if (args.uid) args.uid = parseInt(validator.checkUid(args.uid)); if (args.page) args.page = parseInt(args.page); diff --git a/module/migrate-vijos/script.js b/module/migrate-vijos/script.js index f95f4e556..8bfc0c0d9 100644 --- a/module/migrate-vijos/script.js +++ b/module/migrate-vijos/script.js @@ -1,7 +1,6 @@ /* eslint-disable no-await-in-loop */ const { mongodb } = global.Hydro.nodeModules; const dst = global.Hydro.service.db; -const { problem, discussion } = global.Hydro.model; const map = {}; @@ -13,14 +12,14 @@ const pid = (id) => { }; const tasks = { - user: async (docs) => docs.map((doc) => ({ + user: async (doc) => ({ _id: doc._id, uname: doc.uname, unameLower: doc.uname_lower, salt: doc.salt, hash: doc.hash.split('|')[1], hashType: doc.hash.split('|')[0] === 'vj4' ? 'hydro' : doc.hash.split('|')[0], - role: doc.priv === -1 ? 'root' : 'default', + priv: doc.priv === -1 ? 1 : 0, gravatar: doc.gravatar, email: doc.mail, emailLower: doc.mail_lower, @@ -35,275 +34,221 @@ const tasks = { bio: doc.bio, gender: doc.gender, qq: doc.qq, - })), - problem: async (docs) => { - const problems = []; - for (const doc of docs) { - if (typeof doc.doc_id === 'number') { - map[doc.doc_id] = doc._id; - if (doc.pname) map[doc.pname] = doc._id; - } else { - map[doc.doc_id] = doc.doc_id; - if (doc.pname) map[doc.pname] = doc.doc_id; - } - problems.push({ - _id: doc._id, - pid: doc.pname || pid(doc.doc_id), - title: doc.title, - content: doc.content, - owner: doc.owner_uid, - data: doc.data, - category: doc.category, - tag: doc.tag, - nSubmit: doc.num_submit, - nAccept: doc.num_accept, - hidden: doc.hidden || false, - difficulty: doc.difficulty_admin, - }); - } - return problems; - }, - 'problem.status': async (docs) => { - const problems = []; - for (const doc of docs) { - problems.push({ - pid: pid(doc.doc_id), - rid: doc.rid, - uid: doc.uid, - status: doc.status, - }); - } - return problems; - }, - solution: async (docs) => { - const solutions = []; - for (const doc of docs) { - const reply = doc.reply.map((r) => ({ - owner: r.owner_uid, - content: r.content, - _id: r._id, - })); - solutions.push({ - _id: doc._id, - owner: doc.owner_uid, - content: doc.content, - vote: doc.vote, - reply, - pid: pid(doc.parent_doc_id), - }); - } - return solutions; - }, - discussion: async (docs) => { - const TYPE = { - 10: 'problem', - 20: 'node', - 30: 'contest', - }; - const discussions = []; - for (const doc of docs) { - discussions.push({ - _id: doc._id, - title: doc.title, - content: doc.content, - owner: doc.owner_uid, - views: doc.views, - highlight: doc.highlight, - nReply: doc.num_replies, - ip: doc.ip, - updateAt: doc.update_at, - parentType: TYPE[doc.parent_doc_type], - parentId: doc.parent_doc_id, - }); - } - return discussions; - }, - 'discussion.reply': async (docs) => docs.map((doc) => ({ - _id: doc._id, - owner: doc.owner_uid, - ip: doc.ip, - content: doc.content, - did: doc.parent_doc_id, - })), - 'discussion.node': async (docs) => { - const t = []; - for (const doc of docs) { - if (doc.domain_id === 'system') { - for (const r of doc.content) { - const category = r[0]; - for (const n of r[1]) { - t.push(discussion.addNode(n.name, category)); + }), + document: async (doc) => { + const res = {}; + const mapper = { + _id: '_id', + doc_id: 'docId', + doc_type: 'docType', + num_submit: 'nSubmit', + num_accept: 'nAccept', + difficulty_admin: 'difficulty', + pname: 'pid', + title: 'title', + content: 'content', + owner_uid: 'owner', + category: 'category', + hidden: 'hidden', + data: 'data', + tag: 'tag', + vote: 'vote', + reply: 'reply', + parent_doc_id: 'parentId', + parent_doc_type: 'parentType', + views: 'views', + highlight: 'highlight', + ip: 'ip', + domain_id: 'domainId', + update_at: 'updateAt', + begin_at: 'beginAt', + end_at: 'endAt', + rule: { + field: 'rule', + processer: (rule) => { + const rules = { + 2: 'oi', + 3: 'acm', + 11: 'homework', + }; + return rules[rule]; + }, + }, + pids: { + field: 'pids', + processer: (pids) => pids.map((p) => pid(p)), + }, + attend: 'attend', + desc: 'description', + enroll: 'enroll', + dag: { + field: 'dag', + processer: (dag) => { + const r = []; + for (const t of dag) { + r.push({ + _id: t._id, + title: t.title, + requireNids: t.require_nids, + pids: t.pids.map((id) => pid(id)), + }); } - } - } - } - await Promise.all(t); - return []; - }, - contest: async (docs) => { - const RULES = { - 2: 'oi', - 3: 'acm', - 11: 'homework', + return r; + }, + }, }; - const contests = []; - for (const doc of docs) { - contests.push({ - _id: doc._id, - title: doc.title, - content: doc.content, - owner: doc.owner_uid, - rule: RULES[doc.rule], - beginAt: doc.begin_at, - endAt: doc.end_at, - pids: doc.pids.map((id) => pid(id)), - attend: doc.attend, - }); - } - return contests; - }, - 'contest.status': async (docs) => { - const contests = []; - for (const doc of docs) { - const journal = []; - const detail = []; - for (const i of doc.journal || []) { - const pdoc = await problem.get(pid(i.pid)); - journal.push({ ...i, id: pdoc._id }); + for (const key in doc) { + if (typeof mapper[key] === 'string') { + res[mapper[key]] = doc[key]; + } else if (typeof mapper[key] === 'object') { + res[mapper[key].field] = mapper[key].processer(doc[key]); + } else { + console.log('Unknown key:', key); } - for (const i of doc.detail || []) { - const pdoc = await problem.get(pid(i.pid)); - detail.push({ ...i, id: pdoc._id }); + } + return res; + }, + 'document.status': async (doc) => { + const res = {}; + const mapper = { + _id: '_id', + doc_id: 'docId', + doc_type: 'docType', + uid: 'uid', + domain_id: 'domainId', + num_accept: 'nAccept', + num_submit: 'nSubmit', + rp: 'rp', + status: 'status', + accept: 'accept', + vote: 'vote', + star: 'star', + enroll: 'enroll', + rid: 'rid', + rev: 'rev', + attend: 'attend', + journal: { + field: 'journal', + processer: (journal) => { + const r = []; + for (const i of journal) { + r.push({ ...i, pid: pid(i.pid) }); + } + return r; + }, + }, + detail: { + field: 'detail', + processer: (detail) => { + const r = []; + for (const i of detail) { + r.push({ ...i, pid: pid(i.pid) }); + } + return r; + }, + }, + score: 'score', + time: 'time', + done: 'done', + done_nids: 'doneNids', + done_pids: { + field: 'donePids', + processer: (pids) => pids.map((id) => pid(id)), + }, + }; + for (const key in doc) { + if (typeof mapper[key] === 'string') { + res[mapper[key]] = doc[key]; + } else if (typeof mapper[key] === 'object') { + res[mapper[key].field] = mapper[key].processer(doc[key]); + } else { + console.log('Unknown key:', key); } - contests.push({ - _id: doc._id, - tid: doc.doc_id, - uid: doc.uid, - attend: doc.attend, - rev: doc.rev, - journal, - detail, - score: doc.score, - accept: doc.accept || 0, - time: doc.time || 0, + } + return res; + }, + record: async (doc) => { + const testCases = []; + for (const c of doc.cases) { + testCases.push({ + status: c.status, + score: c.score, + time: c.time, + memory: c.memory, + judge_text: c.judge_text, }); } - return contests; + return { + _id: doc._id, + status: doc.status, + score: doc.score, + time: doc.time_ms, + memory: doc.memory_kb, + code: doc.code, + lang: doc.lang, + uid: doc.uid, + pid: pid(doc.pid), + tid: doc.tid, + domainId: doc.domain_id, + judger: doc.judge_uid, + judgeAt: doc.judge_at, + judgeTexts: doc.judge_texts, + compileTexts: doc.compile_texts, + rejudged: doc.rejudged, + testCases, + }; }, - training: async (docs) => { - const trainings = []; - for (const doc of docs) { - const dag = []; - for (const t of doc.dag) { - dag.push({ - _id: t._id, - title: t.title, - requireNids: t.require_nids, - pids: t.pids.map((id) => pid(id)), - }); - } - trainings.push({ - _id: doc._id, - title: doc.title, - content: doc.content, - description: doc.desc, - enroll: doc.enroll, - owner: doc.owner_uid, - dag, - }); - } - return trainings; - }, - 'training.status': async (docs) => { - const trainings = []; - for (const doc of docs) { - trainings.push({ - _id: doc._id, - pid: pid(doc.doc_id), - rid: doc.rid, - uid: doc.uid, - done: doc.done, - enroll: doc.enroll, - tid: doc.doc_id, - doneNids: doc.done_nids, - donePids: doc.done_pids.map((id) => pid(id)), - }); - } - return trainings; - }, - record: async (docs) => { - const records = []; - for (const doc of docs) { - const testCases = []; - for (const c of doc.cases) { - testCases.push({ - status: c.status, - score: c.score, - time: c.time, - memory: c.memory, - judge_text: c.judge_text, - }); - } - records.push({ - _id: doc._id, - status: doc.status, - score: doc.score, - time: doc.time_ms, - memory: doc.memory_kn, - code: doc.code, - lang: doc.lang, - uid: doc.uid, - pid: pid(doc.pid), - tid: doc.tid, - judger: doc.judge_uid, - judgeAt: doc.judge_at, - judgeTexts: doc.judge_texts, - compileTexts: doc.compile_texts, - rejudged: doc.rejudged, - testCases, - }); - } - return records; - }, - 'fs.files': async (docs) => docs, - 'fs.chunks': async (docs) => docs, - file: async (docs) => docs.map((doc) => ({ + 'fs.files': async (doc) => doc, + 'fs.chunks': async (doc) => doc, + file: async (doc) => ({ _id: doc._id, count: doc.metadata.link, secret: doc.metadata.secret, size: doc.length, md5: doc.md5, - })), + }), }; const cursor = { user: (s) => s.collection('user').find(), - problem: (s) => s.collection('document').find({ doc_type: 10 }), - 'problem.status': (s) => s.collection('document.status').find({ doc_type: 10 }), - solution: (s) => s.collection('document').find({ doc_type: 11 }), - discussion: (s) => s.collection('document').find({ doc_type: 21 }), - 'discussion.reply': (s) => s.collection('document').find({ doc_type: 22 }), - 'discussion.node': (s) => s.collection('document').find({ doc_type: 20 }), - contest: (s) => s.collection('document').find({ doc_type: 30 }), - 'contest.status': (s) => s.collection('document.status').find({ doc_type: 30 }), - training: (s) => s.collection('document').find({ doc_type: 40 }), - 'training.status': (s) => s.collection('document.status').find({ doc_type: 40 }), + document: (s) => s.collection('document').find(), + 'document.status': (s) => s.collection('document.status').find(), record: (s) => s.collection('record').find(), 'fs.files': (s) => s.collection('fs.files').find(), 'fs.chunks': (s) => s.collection('fs.chunks').find(), file: (s) => s.collection('fs.files').find(), }; +async function fixPid(report) { + const count = await dst.collection('document').find({ docType: 10 }).count(); + await report({ progress: 1, message: `Fix pid: ${count}` }); + const total = Math.floor(count / 50); + for (let i = 0; i <= total; i++) { + const docs = await dst.collection('document') + .find({ docType: 10 }).skip(i * 50).limit(50) + .toArray(); + for (const doc of docs) { + dst.collection('document').updateOne( + { _id: doc._id }, + { $set: { pid: doc.pid || doc.docId.toString() } }, + ); + } + await report({ progress: Math.round(100 * ((i + 1) / (total + 1))) }); + } +} + async function task(name, src, report) { const count = await cursor[name](src).count(); await report({ progress: 1, message: `${name}: ${count}` }); const total = Math.floor(count / 50); for (let i = 0; i <= total; i++) { const docs = await cursor[name](src).skip(i * 50).limit(50).toArray(); - const t = await tasks[name](docs); - if (t.length) await dst.collection(name).insertMany(t); + const res = []; + for (const doc of docs) res.push(tasks[name](doc)); + if (res.length) await dst.collection(name).insertMany(await Promise.all(res)); await report({ progress: Math.round(100 * ((i + 1) / (total + 1))) }); } + await fixPid(report); } async function migrateVijos({ @@ -336,19 +281,8 @@ async function migrateVijos({ } await dst.collection('system').insertOne({ _id: 'migrateVijosFs', value: true }); } - const d = [ - 'problem', 'problem.status', 'solution', 'discussion', 'discussion.reply', - 'discussion.node', 'contest', 'training', 'training.status', 'record', 'file', - ]; - for (const i of d) { - await dst.collection(i).deleteMany(); - } - const t = [ - 'user', 'problem', 'problem.status', 'solution', 'discussion', - 'discussion.reply', 'discussion.node', 'contest', 'training', 'training.status', - 'record', 'file', - ]; - for (const i of t) await task(i, src, report); + const d = ['user', 'document', 'document.status', 'record', 'file']; + for (const i of d) await task(i, src, report); } global.Hydro.script.migrateVijos = module.exports = { run: migrateVijos }; diff --git a/templates/components/problem.html b/templates/components/problem.html index 846ca3cc9..d950e476c 100644 --- a/templates/components/problem.html +++ b/templates/components/problem.html @@ -2,7 +2,7 @@ {%- if not invalid %}
- {{ _('View Details') }} + {{ _('View Details') }}
- {% if tsdict[tdoc._id].attend %} + {% if tsdict[tdoc.docId].attend %}

{{ _('Attended') }}

{% else %}

{{ _('Not Attended') }}

@@ -88,7 +88,7 @@
-

{{ tdoc.title }}

+

{{ tdoc.title }}

  • {{ model.contest.RULES[tdoc.rule].TEXT }} @@ -99,7 +99,7 @@
  • {{ tdoc['attend']|default(0) }}
  • - {% if tsdict[tdoc._id]['attend'] == 1 %} + {% if tsdict[tdoc.docId]['attend'] == 1 %}
  • {{ _('Attended') }}
  • diff --git a/templates/contest_scoreboard.html b/templates/contest_scoreboard.html index bfd022ebd..5b8d72b07 100644 --- a/templates/contest_scoreboard.html +++ b/templates/contest_scoreboard.html @@ -4,10 +4,10 @@
    @@ -23,7 +23,7 @@ {%- for column in rows[0] -%} {% if column.type == 'problem_detail' %} - {{ column['value'] }} + {{ column['value'] }} {% else %} {{ column.value }} {% endif %} diff --git a/templates/main.html b/templates/main.html index fc14a63fc..bc48f6136 100644 --- a/templates/main.html +++ b/templates/main.html @@ -32,7 +32,7 @@
    -

    {{ tdoc.title }}

    +

    {{ tdoc.title }}

    • {{ model.contest.RULES[tdoc.rule].TEXT }} @@ -43,7 +43,7 @@
    • {{ tdoc['attend']|default(0) }}
    • - {% if tsdict[tdoc._id]['attend'] == 1 %} + {% if tsdict[tdoc.docId]['attend'] == 1 %}
    • {{ _('Attended') }}
    • @@ -74,7 +74,7 @@
    -

    {{ tdoc.title }}

    +

    {{ tdoc.title }}

    {{ tdoc['content'] }}

    @@ -83,10 +83,10 @@ {{ _('{0} sections').format(tdoc['dag']|length) }}, {{ _('{0} problems').format(model.training.getPids(tdoc)|length) }}
  • - {% if tsdict[tdoc._id]['enroll'] %} - {% if not tsdict[tdoc._id]['done'] %} + {% if tsdict[tdoc.docId]['enroll'] %} + {% if not tsdict[tdoc.docId]['done'] %} - {{ _('Completed') }} {{ (100 * tsdict[tdoc._id]['donePids']|length / model.training.getPids(tdoc)|length)|round|int }}% + {{ _('Completed') }} {{ (100 * tsdict[tdoc.docId]['donePids']|length / model.training.getPids(tdoc)|length)|round|int }}% {% else %} {{ _('Completed') }} 100% diff --git a/templates/partials/contest_sidebar.html b/templates/partials/contest_sidebar.html index 3247aeeeb..bd97dc8c6 100644 --- a/templates/partials/contest_sidebar.html +++ b/templates/partials/contest_sidebar.html @@ -2,7 +2,7 @@ {% import "components/contest.html" as contest with context %}
    {% if page_name != 'contest_detail' %} - +

    {{ tdoc.title }}

    @@ -18,14 +18,14 @@
    -

    {{ tdoc.title }}

    +

    {{ tdoc.title }}

    {{ tdoc['content'] }}

    @@ -30,10 +30,10 @@ {{ _('{0} sections').format(tdoc['dag']|length) }}, {{ _('{0} problems').format(model.training.getPids(tdoc)|length) }}
  • - {% if tsdict[tdoc._id]['enroll'] %} - {% if not tsdict[tdoc._id]['done'] %} + {% if tsdict[tdoc.docId]['enroll'] %} + {% if not tsdict[tdoc.docId]['done'] %} - {{ _('Completed') }} {{ (100 * tsdict[tdoc._id]['done_pids']|length / model.training.getPids(tdoc)|length)|round|int }}% + {{ _('Completed') }} {{ (100 * tsdict[tdoc.docId]['done_pids']|length / model.training.getPids(tdoc)|length)|round|int }}% {% else %} {{ _('Completed') }} 100%