!UNSTABLE 优化数据库结构,为domain功能准备

This commit is contained in:
masnn committed 2020-05-29 13:05:52 +08:00
1 parent 7d27e56018
commit fff2172c38
40 files changed
+1411 -1012

No files matched your search

-1
View File
@@ -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",
+53 -46
View File
@@ -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}`;
}
+44 -47
View File
@@ -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}`;
}
}
+14 -14
View File
@@ -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];
+5 -5
View File
@@ -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`;
}
}
+9 -9
View File
@@ -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 {
+13 -13
View File
@@ -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', '/'],
+84 -71
View File
@@ -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`;
+23 -15
View File
@@ -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;
+15 -15
View File
@@ -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) {
+10 -8
View File
@@ -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 */
+1
View File
@@ -20,6 +20,7 @@ exports.udoc = {
viewLang: 'zh_CN',
loginat: new Date(),
loginip: '0.0.0.0',
priv: 0,
};
exports.pdoc = {
+7 -3
View File
@@ -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();
+2 -2
View File
@@ -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,
};
+5 -5
View File
@@ -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 = {
探索: [
+53 -42
View File
@@ -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
+75 -92
View File
@@ -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;
}
+366
View File
@@ -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,
};
+96
View File
@@ -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,
};
+1
View File
@@ -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'),
+2 -2
View File
@@ -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,
};
+70 -92
View File
@@ -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,
};
+40 -27
View File
@@ -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,
+39 -66
View File
@@ -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;
}
+56 -58
View File
@@ -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,
+59 -60
View File
@@ -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,
};
+3
View File
@@ -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;
+27 -18
View File
@@ -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) {
+13 -9
View File
@@ -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);
+189 -255
View File
@@ -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 };
+1 -1
View File
@@ -2,7 +2,7 @@
{%- if not invalid %}
<a
{%- if tdoc %}
href="/c/{{ tdoc._id }}/p/{{ pdoc._id }}"
href="/c/{{ tdoc.docId }}/p/{{ pdoc.docId }}"
{%- else %}
href="/p/{{ pdoc.pid }}"
{%- endif %}
+4 -4
View File
@@ -32,9 +32,9 @@
</div>
<div class="media__right middle">
<div class="section__body">
<a href="/c/{{ tdoc._id }}" class="detail-button">{{ _('View Details') }}</a>
<a href="/c/{{ tdoc.docId }}" class="detail-button">{{ _('View Details') }}</a>
<div class="typo text-center">
{% if tsdict[tdoc._id].attend %}
{% if tsdict[tdoc.docId].attend %}
<p><span class="icon icon-check"></span> {{ _('Attended') }}</p>
{% else %}
<p><span class="icon icon-schedule"></span> {{ _('Not Attended') }}</p>
@@ -88,7 +88,7 @@
</div>
</div>
<div class="media__body medium">
<h1 class="contest__title"><a href="/c/{{ tdoc._id }}" data-emoji-enabled>{{ tdoc.title }}</a></h1>
<h1 class="contest__title"><a href="/c/{{ tdoc.docId }}" data-emoji-enabled>{{ tdoc.title }}</a></h1>
<ul class="supplementary list">
<li>
<a href="?rule={{ tdoc['rule'] }}" class="contest-type-tag"><span class="icon icon-award"></span>{{ model.contest.RULES[tdoc.rule].TEXT }}</a>
@@ -99,7 +99,7 @@
<li>
<span class="icon icon-user--multiple"></span> {{ tdoc['attend']|default(0) }}
</li>
{% if tsdict[tdoc._id]['attend'] == 1 %}
{% if tsdict[tdoc.docId]['attend'] == 1 %}
<li class="contest__info-attended">
<span class="icon icon-check"></span> {{ _('Attended') }}
</li>
+3 -3
View File
@@ -4,10 +4,10 @@
<div class="row"><div class="medium-12 columns">
<div class="section visible">
<div class="section__header">
<a class="button" target="_blank" href="/c/{{ tdoc._id }}/export/html">
<a class="button" target="_blank" href="/c/{{ tdoc.docId }}/export/html">
<span class="icon icon-download"></span> {{ _('Export as HTML') }}
</a>
<a class="button" href="/c/{{ tdoc._id }}/export/csv">
<a class="button" href="/c/{{ tdoc.docId }}/export/csv">
<span class="icon icon-download"></span> {{ _('Export as CSV') }}
</a>
</div>
@@ -23,7 +23,7 @@
{%- for column in rows[0] -%}
<th class="col--{{ column['type'] }}">
{% if column.type == 'problem_detail' %}
<a href="/c/{{ tdoc._id }}/p/{{ column.raw._id }}" data-tooltip="{{ column['raw']['title'] }}">{{ column['value'] }}</a>
<a href="/c/{{ tdoc.docId }}/p/{{ column.raw._id }}" data-tooltip="{{ column['raw']['title'] }}">{{ column['value'] }}</a>
{% else %}
{{ column.value }}
{% endif %}
+6 -6
View File
@@ -32,7 +32,7 @@
</div>
</div>
<div class="media__body medium">
<h1 class="contest__title"><a href="/c/{{ tdoc._id }}" data-emoji-enabled>{{ tdoc.title }}</a></h1>
<h1 class="contest__title"><a href="/c/{{ tdoc.docId }}" data-emoji-enabled>{{ tdoc.title }}</a></h1>
<ul class="supplementary list">
<li>
<a href="/c?rule={{ tdoc['rule'] }}" class="contest-type-tag"><span class="icon icon-award"></span>{{ model.contest.RULES[tdoc.rule].TEXT }}</a>
@@ -43,7 +43,7 @@
<li>
<span class="icon icon-user--multiple"></span> {{ tdoc['attend']|default(0) }}
</li>
{% if tsdict[tdoc._id]['attend'] == 1 %}
{% if tsdict[tdoc.docId]['attend'] == 1 %}
<li class="contest__info-attended">
<span class="icon icon-check"></span> {{ _('Attended') }}
</li>
@@ -74,7 +74,7 @@
</div>
</div>
<div class="media__body middle">
<h1 class="training__title"><a href="/t/{{ tdoc._id }}" data-emoji-enabled>{{ tdoc.title }}</a></h1>
<h1 class="training__title"><a href="/t/{{ tdoc.docId }}" data-emoji-enabled>{{ tdoc.title }}</a></h1>
<div class="training__intro typo">
<p>{{ tdoc['content'] }}</p>
</div>
@@ -83,10 +83,10 @@
<span class="icon icon-flag text-blue"></span> {{ _('{0} sections').format(tdoc['dag']|length) }}, {{ _('{0} problems').format(model.training.getPids(tdoc)|length) }}
</li>
<li>
{% if tsdict[tdoc._id]['enroll'] %}
{% if not tsdict[tdoc._id]['done'] %}
{% if tsdict[tdoc.docId]['enroll'] %}
{% if not tsdict[tdoc.docId]['done'] %}
<span class="icon training-status--icon progress"></span>
<span class="training-status--text progress">{{ _('Completed') }} {{ (100 * tsdict[tdoc._id]['donePids']|length / model.training.getPids(tdoc)|length)|round|int }}%</span>
<span class="training-status--text progress">{{ _('Completed') }} {{ (100 * tsdict[tdoc.docId]['donePids']|length / model.training.getPids(tdoc)|length)|round|int }}%</span>
{% else %}
<span class="icon training-status--icon done"></span>
<span class="training-status--text done">{{ _('Completed') }} 100%</span>
+8 -8
View File
@@ -2,7 +2,7 @@
{% import "components/contest.html" as contest with context %}
<div class="section side">
{% if page_name != 'contest_detail' %}
<a class="contest-sidebar__bg" href="/c/{{ tdoc._id }}">
<a class="contest-sidebar__bg" href="/c/{{ tdoc.docId }}">
<div class="section__body">
<h1>{{ tdoc.title }}</h1>
<div class="contest-sidebar__status">
@@ -18,14 +18,14 @@
<div>
<ol class="menu">
{% if page_name != 'contest_detail' %}
<li class="menu__item"><a class="menu__link" href="/c/{{ tdoc._id }}">
<li class="menu__item"><a class="menu__link" href="/c/{{ tdoc.docId }}">
<span class="icon icon-award"></span> {{ _('View Contest') }}
</a></li>
{% endif %}
{% if not attended and not model.contest.isDone(tdoc) %}
<li class="menu__item">
{% if handler.hasPerm(perm.PERM_ATTEND_CONTEST) and handler.hasPerm(perm.PERM_LOGGEDIN) %}
<form action="/c/{{ tdoc._id }}" method="POST">
<form action="/c/{{ tdoc.docId }}" method="POST">
<input type="hidden" name="operation" value="attend">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="menu__link" type="submit">
@@ -44,25 +44,25 @@
</li>
{% endif %}
{% if handler.canShowScoreboard(tdoc, False) %}
<li class="menu__item"><a class="menu__link" href="/c/{{ tdoc._id }}/scoreboard">
<li class="menu__item"><a class="menu__link" href="/c/{{ tdoc.docId }}/scoreboard">
<span class="icon icon-statistics"></span> {{ _('Scoreboard') }}
</a></li>
{% elif handler.canViewHiddenScoreboard(tdoc) %}
<li class="menu__item"><a class="menu__link" href="/c/{{ tdoc._id }}/scoreboard">
<li class="menu__item"><a class="menu__link" href="/c/{{ tdoc.docId }}/scoreboard">
<span class="icon icon-statistics"></span> {{ _('Scoreboard (Hidden)') }}
</a></li>
{% endif %}
{% if user._id == tdoc.owner or handler.hasPerm(perm.PERM_EDIT_CONTEST) %}
<li class="menu__item"><a class="menu__link" href="/c/{{ tdoc._id }}/edit">
<li class="menu__item"><a class="menu__link" href="/c/{{ tdoc.docId }}/edit">
<span class="icon icon-edit"></span> {{ _('Edit Contest') }}
</a></li>
{% endif %}
{% if handler.hasPerm(perm.PERM_READ_RECORD_CODE) %}
<li class="menu__item"><a class="menu__link" href="/c/{{ tdoc._id }}/code">
<li class="menu__item"><a class="menu__link" href="/c/{{ tdoc.docId }}/code">
<span class="icon icon-download"></span> {{ _('Export All Code') }}
</a></li>
{% endif %}
<li class="menu__item"><a class="menu__link" href="/r?tid={{ tdoc._id }}">
<li class="menu__item"><a class="menu__link" href="/r?tid={{ tdoc.docId }}">
<span class="icon icon-flag"></span> {{ _('All Submissions') }}
</a></li>
<li class="menu__item"><a class="menu__link" href="/wiki/help#contest">
@@ -16,11 +16,11 @@
<span class="icon icon-enlarge"></span> {{ _('Quit Scratchpad') }} (Alt+Q)
</a></li>
{% else %}
<li class="menu__item"><a class="menu__link" href="/c/{{ tdoc._id }}/p/{{ pdoc._id }}">
<li class="menu__item"><a class="menu__link" href="/c/{{ tdoc.docId }}/p/{{ pdoc.docId }}">
<span class="icon icon-flag"></span> {{ _('View Problem') }}
</a></li>
{% endif %}
<li class="menu__item scratchpad--hide"><a class="menu__link{% if page_name == 'contest_detail_problem_submit' %} active{% endif %}" href="/c/{{ tdoc._id }}/p/{{ pdoc._id }}/submit">
<li class="menu__item scratchpad--hide"><a class="menu__link{% if page_name == 'contest_detail_problem_submit' %} active{% endif %}" href="/c/{{ tdoc.docId }}/p/{{ pdoc.docId }}/submit">
<span class="icon icon-send"></span> {{ _('Submit') }}
</a></li>
{% elif model.contest.isDone(tdoc) %}
@@ -10,7 +10,7 @@
<ol class="menu">
{% if page_name == 'discussion_node' or page_name == 'discussion_detail' %}
{% if handler.hasPerm(perm.PERM_CREATE_DISCUSSION) %}
<li class="menu__item"><a class="menu__link highlight" href="/discuss/problem/{{ pdoc._id }}/create">
<li class="menu__item"><a class="menu__link highlight" href="/discuss/problem/{{ pdoc.docId }}/create">
<span class="icon icon-add"></span> {{ _('Create a Discussion') }}
</a></li>
{% else %}
@@ -55,7 +55,7 @@
<li class="menu__seperator"></li>
{% endif %}
{% if handler.hasPerm(perm.PERM_VIEW_DISCUSSION) %}
<li class="menu__item"><a class="menu__link{% if page_name == 'discussion_node' or page_name == 'discussion_detail' %} active{% endif %}" href="/discuss/problem/{{ pdoc._id }}">
<li class="menu__item"><a class="menu__link{% if page_name == 'discussion_node' or page_name == 'discussion_detail' %} active{% endif %}" href="/discuss/problem/{{ pdoc.docId }}">
<span class="icon icon-comment--text"></span> {{ _('Discussions') }}
</a></li>
{% endif %}
@@ -84,7 +84,7 @@
<div class="section__body typo">
<dl class="large horizontal">
<dt>{{ _('ID') }}</dt>
<dd>{{ pdoc['pid'] }}</dd>
<dd>{{ pdoc.docId }}</dd>
{% if page_name != 'discussion_node' and page_name != 'discussion_detail' %}
{% if handler.hasPerm(perm.PERM_LOGGEDIN) %}
<dt>{{ _('Submission') }}</dt>
@@ -125,11 +125,11 @@
{{ problem.render_problem_tags(pdoc, true) }}
</dd>
<dt>{{ _('# Submissions') }}</dt>
<dd><a href="/r?pid={{ pdoc['_id'] }}">{{ pdoc.nSubmit }}</a></dd>
<dd><a href="/r?pid={{ pdoc.docId }}">{{ pdoc.nSubmit }}</a></dd>
{% if page_name != 'discussion_node' and page_name != 'discussion_detail' %}
{% if handler.hasPerm(perm.PERM_LOGGEDIN) %}
<dt>{{ _('# My Submissions') }}</dt>
<dd><a href="/r?pid={{ pdoc['_id'] }}&uid_or_name={{ handler.user['_id'] }}">{{ pdoc['psdoc'].nSubmit|default(0) }}</a></dd>
<dd><a href="/r?pid={{ pdoc.docId }}&uid_or_name={{ handler.user['_id'] }}">{{ pdoc['psdoc'].nSubmit|default(0) }}</a></dd>
{% endif %}
{% endif %}
<dt>{{ _('Accepted') }}</dt>
@@ -150,13 +150,13 @@
{% if tdocs %}
<p>{{ _('In following training plans') }}: </p>
{% for tdoc in tdocs %}
<p><a href="/t/{{ tdoc._id }}">{{ tdoc.title }}</a></p>
<p><a href="/t/{{ tdoc.docId }}">{{ tdoc.title }}</a></p>
{% endfor %}
{% endif %}
{% if ctdocs %}
<p>{{ _('In following contests') }}: </p>
{% for tdoc in ctdocs %}
<p><a href="/c/{{ tdoc._id }}">{{ tdoc.title }}</a></p>
<p><a href="/c/{{ tdoc.docId }}">{{ tdoc.title }}</a></p>
{% endfor %}
{% endif %}
</div>
+1 -1
View File
@@ -66,7 +66,7 @@
{% if tdoc %}
<dt>{{ _('Contest') }}</dt>
<dd>
<a href="/c/{{ tdoc._id }}">{{ tdoc.title }}</a>
<a href="/c/{{ tdoc.docId }}">{{ tdoc.title }}</a>
</dd>
{% endif %}
{% if pdoc and (user._id == pdoc.owner or handler.hasPerm(perm.PERM_READ_PROBLEM_DATA)) %}
+1 -1
View File
@@ -175,7 +175,7 @@
</li>
{% endif %}
{% if tdoc.owner == user._id or handler.hasPerm(perm.PERM_EDIT_TRAINING) %}
<li class="menu__item"><a class="menu__link" href="/t/{{ tdoc._id }}/edit">
<li class="menu__item"><a class="menu__link" href="/t/{{ tdoc.docId }}/edit">
<span class="icon icon-edit"></span> {{ _('Edit') }}
</a></li>
{% endif %}
+4 -4
View File
@@ -21,7 +21,7 @@
</div>
</div>
<div class="media__body middle">
<h1 class="training__title"><a href="/t/{{ tdoc._id }}" data-emoji-enabled>{{ tdoc.title }}</a></h1>
<h1 class="training__title"><a href="/t/{{ tdoc.docId }}" data-emoji-enabled>{{ tdoc.title }}</a></h1>
<div class="training__intro typo">
<p>{{ tdoc['content'] }}</p>
</div>
@@ -30,10 +30,10 @@
<span class="icon icon-flag text-blue"></span> {{ _('{0} sections').format(tdoc['dag']|length) }}, {{ _('{0} problems').format(model.training.getPids(tdoc)|length) }}
</li>
<li>
{% if tsdict[tdoc._id]['enroll'] %}
{% if not tsdict[tdoc._id]['done'] %}
{% if tsdict[tdoc.docId]['enroll'] %}
{% if not tsdict[tdoc.docId]['done'] %}
<span class="icon training-status--icon progress"></span>
<span class="training-status--text progress">{{ _('Completed') }} {{ (100 * tsdict[tdoc._id]['done_pids']|length / model.training.getPids(tdoc)|length)|round|int }}%</span>
<span class="training-status--text progress">{{ _('Completed') }} {{ (100 * tsdict[tdoc.docId]['done_pids']|length / model.training.getPids(tdoc)|length)|round|int }}%</span>
{% else %}
<span class="icon training-status--icon done"></span>
<span class="training-status--text done">{{ _('Completed') }} 100%</span>