mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
core: database structure update
This commit is contained in:
1 parent
dca8e28361
commit
4daf8bac80
24 files changed
+182
-263
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "2.38.0",
|
||||
"version": "2.39.0",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "src/loader",
|
||||
"module": "src/loader",
|
||||
@@ -36,7 +36,7 @@
|
||||
"moment-timezone": "^0.5.33",
|
||||
"mongodb": "^3.7.1",
|
||||
"nanoid": "^3.1.25",
|
||||
"nodemailer": "^6.6.3",
|
||||
"nodemailer": "^6.6.4",
|
||||
"p-queue": "^7.1.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"serialize-javascript": "^6.0.0",
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
import paginate from '../lib/paginate';
|
||||
import { PERM, PRIV } from '../model/builtin';
|
||||
import * as contest from '../model/contest';
|
||||
import * as document from '../model/document';
|
||||
import domain from '../model/domain';
|
||||
import message from '../model/message';
|
||||
import problem from '../model/problem';
|
||||
@@ -32,19 +31,15 @@ export class ContestListHandler extends Handler {
|
||||
@param('rule', Types.Range(contest.RULES), true)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, rule = '', page = 1) {
|
||||
const cursor = contest.getMulti(domainId, rule ? { rule } : undefined);
|
||||
const cursor = contest.getMulti(domainId, rule ? { rule } : { rule: { $ne: 'homework' } });
|
||||
const qs = rule ? `rule=${rule}` : '';
|
||||
const [tdocs, tpcount] = await paginate(cursor, page, system.get('pagination.contest'));
|
||||
const tids = [];
|
||||
for (const tdoc of tdocs) tids.push(tdoc.docId);
|
||||
const tsdict = await contest.getListStatus(domainId, this.user._id, tids);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['contest_main', null],
|
||||
];
|
||||
this.response.template = 'contest_main.html';
|
||||
this.response.body = {
|
||||
page, tpcount, qs, rule, tdocs, tsdict, path,
|
||||
page, tpcount, qs, rule, tdocs, tsdict,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -68,17 +63,12 @@ export class ContestDetailHandler extends Handler {
|
||||
if (contest.canShowSelfRecord.call(this, tdoc)) {
|
||||
const q = [];
|
||||
for (const i in psdict) q.push(psdict[i].rid);
|
||||
rdict = await record.getList(domainId, q, true);
|
||||
rdict = await record.getList(domainId, q);
|
||||
} else {
|
||||
for (const i in psdict) rdict[psdict[i].rid] = { _id: psdict[i].rid };
|
||||
}
|
||||
} else attended = false;
|
||||
const udict = await user.getList(domainId, [tdoc.owner]);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['contest_main', 'contest_main'],
|
||||
[tdoc.title, null, null, true],
|
||||
];
|
||||
const index = tdoc.pids.map((i) => i.toString());
|
||||
for (const key in pdict) {
|
||||
pdict[key].tag.length = 0;
|
||||
@@ -86,7 +76,7 @@ export class ContestDetailHandler extends Handler {
|
||||
if (i !== '9') pdict[key].pid = i;
|
||||
}
|
||||
this.response.body = {
|
||||
path, tdoc, tsdoc, attended, udict, pdict, psdict, rdict, page,
|
||||
tdoc, tsdoc, attended, udict, pdict, psdict, rdict, page,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -258,7 +248,7 @@ export class ContestEditHandler extends Handler {
|
||||
}
|
||||
|
||||
export class ContestProblemHandler extends Handler {
|
||||
tdoc: Tdoc<30 | 60>;
|
||||
tdoc: Tdoc<30>;
|
||||
pdoc: ProblemDoc;
|
||||
tsdoc: any;
|
||||
udoc: User;
|
||||
@@ -391,10 +381,7 @@ export class ContestDetailProblemSubmitHandler extends ContestProblemHandler {
|
||||
throw new BadRequestError('Language not allowed.');
|
||||
}
|
||||
await this.limitRate('add_record', 60, system.get('limit.submission'));
|
||||
const rid = await record.add(domainId, pid, this.user._id, lang, code, true, pretest ? input : {
|
||||
type: document.TYPE_CONTEST,
|
||||
tid,
|
||||
});
|
||||
const rid = await record.add(domainId, pid, this.user._id, lang, code, true, pretest ? input : tid);
|
||||
const rdoc = await record.get(domainId, rid);
|
||||
if (!rdoc) throw new RecordNotFoundError(domainId, rid);
|
||||
if (!pretest) {
|
||||
|
||||
@@ -19,7 +19,7 @@ export const typeMapper = {
|
||||
contest: document.TYPE_CONTEST,
|
||||
node: document.TYPE_DISCUSSION_NODE,
|
||||
training: document.TYPE_TRAINING,
|
||||
homework: document.TYPE_HOMEWORK,
|
||||
homework: document.TYPE_CONTEST,
|
||||
};
|
||||
|
||||
class DiscussionHandler extends Handler {
|
||||
|
||||
@@ -8,9 +8,7 @@ import {
|
||||
import type { DomainDoc } from '../interface';
|
||||
import avatar from '../lib/avatar';
|
||||
import paginate from '../lib/paginate';
|
||||
import {
|
||||
PERM, PERMS_BY_FAMILY, PRIV,
|
||||
} from '../model/builtin';
|
||||
import { PERM, PERMS_BY_FAMILY, PRIV } from '../model/builtin';
|
||||
import * as discussion from '../model/discussion';
|
||||
import domain from '../model/domain';
|
||||
import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting';
|
||||
|
||||
@@ -14,7 +14,6 @@ import BlackListModel from '../model/blacklist';
|
||||
import { PERM, PRIV } from '../model/builtin';
|
||||
import * as contest from '../model/contest';
|
||||
import * as discussion from '../model/discussion';
|
||||
import * as document from '../model/document';
|
||||
import domain from '../model/domain';
|
||||
import message from '../model/message';
|
||||
import ProblemModel from '../model/problem';
|
||||
@@ -39,11 +38,10 @@ class HomeHandler extends Handler {
|
||||
|
||||
async getHomework(domainId: string, limit = 5) {
|
||||
if (this.user.hasPerm(PERM.PERM_VIEW_HOMEWORK)) {
|
||||
const tdocs = await contest.getMulti(domainId, {}, document.TYPE_HOMEWORK)
|
||||
const tdocs = await contest.getMulti(domainId, { rule: 'homework' })
|
||||
.limit(limit).toArray();
|
||||
const tsdict = await contest.getListStatus(
|
||||
domainId, this.user._id,
|
||||
tdocs.map((tdoc) => tdoc.docId), document.TYPE_HOMEWORK,
|
||||
domainId, this.user._id, tdocs.map((tdoc) => tdoc.docId),
|
||||
);
|
||||
return ['homework', tdocs, tsdict];
|
||||
}
|
||||
@@ -52,7 +50,7 @@ class HomeHandler extends Handler {
|
||||
|
||||
async getContest(domainId: string, limit = 10) {
|
||||
if (this.user.hasPerm(PERM.PERM_VIEW_CONTEST)) {
|
||||
const tdocs = await contest.getMulti(domainId)
|
||||
const tdocs = await contest.getMulti(domainId, { rule: { $ne: 'homework' } })
|
||||
.limit(limit).toArray();
|
||||
const tsdict = await contest.getListStatus(
|
||||
domainId, this.user._id, tdocs.map((tdoc) => tdoc.docId),
|
||||
|
||||
@@ -6,17 +6,16 @@ import moment from 'moment-timezone';
|
||||
import { ObjectID } from 'mongodb';
|
||||
import { Time } from '@hydrooj/utils/lib/utils';
|
||||
import {
|
||||
BadRequestError, HomeworkNotAttendedError, HomeworkNotLiveError,
|
||||
ProblemNotFoundError, ValidationError,
|
||||
BadRequestError, ContestNotFoundError, HomeworkNotAttendedError,
|
||||
HomeworkNotLiveError, ProblemNotFoundError, ValidationError,
|
||||
} from '../error';
|
||||
import {
|
||||
DomainDoc, PenaltyRules, ProblemDoc, Tdoc, User,
|
||||
DomainDoc, PenaltyRules, ProblemDoc, ProblemId, Tdoc, User,
|
||||
} from '../interface';
|
||||
import paginate from '../lib/paginate';
|
||||
import { PERM, PRIV, STATUS } from '../model/builtin';
|
||||
import * as contest from '../model/contest';
|
||||
import * as discussion from '../model/discussion';
|
||||
import * as document from '../model/document';
|
||||
import domain from '../model/domain';
|
||||
import problem from '../model/problem';
|
||||
import record from '../model/record';
|
||||
@@ -33,7 +32,7 @@ const convertPenaltyRules = validatePenaltyRules;
|
||||
|
||||
class HomeworkMainHandler extends Handler {
|
||||
async get({ domainId }) {
|
||||
const tdocs = await contest.getMulti(domainId, {}, document.TYPE_HOMEWORK).toArray();
|
||||
const tdocs = await contest.getMulti(domainId, { rule: 'homework' }).toArray();
|
||||
const calendar = [];
|
||||
for (const tdoc of tdocs) {
|
||||
const cal = { ...tdoc, url: this.url('homework_detail', { tid: tdoc.docId }) };
|
||||
@@ -43,11 +42,7 @@ class HomeworkMainHandler extends Handler {
|
||||
} else cal.endAt = tdoc.penaltySince;
|
||||
calendar.push(cal);
|
||||
}
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['homework_main', null],
|
||||
];
|
||||
this.response.body = { tdocs, calendar, path };
|
||||
this.response.body = { tdocs, calendar };
|
||||
this.response.template = 'homework_main.html';
|
||||
}
|
||||
}
|
||||
@@ -56,9 +51,10 @@ class HomeworkDetailHandler extends Handler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, tid: ObjectID, page = 1) {
|
||||
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
|
||||
const tdoc = await contest.get(domainId, tid);
|
||||
if (tdoc.rule !== 'homework') throw new ContestNotFoundError(domainId, tid);
|
||||
const [tsdoc, pdict] = await Promise.all([
|
||||
contest.getStatus(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK),
|
||||
contest.getStatus(domainId, tdoc.docId, this.user._id),
|
||||
problem.getList(domainId, tdoc.pids, true),
|
||||
]);
|
||||
const psdict = {};
|
||||
@@ -74,7 +70,6 @@ class HomeworkDetailHandler extends Handler {
|
||||
rdict = await record.getList(
|
||||
domainId,
|
||||
tsdoc.journal.map((pdetail) => pdetail.rid),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -87,11 +82,6 @@ class HomeworkDetailHandler extends Handler {
|
||||
const uids = ddocs.map((ddoc) => ddoc.owner);
|
||||
uids.push(tdoc.owner);
|
||||
const udict = await user.getList(domainId, uids);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['homework_main', 'homework_main'],
|
||||
[tdoc.title, null, null, true],
|
||||
];
|
||||
const index = tdoc.pids.map((i) => i.toString());
|
||||
for (const key in pdict) {
|
||||
const i = (index.indexOf(key) + 10).toString(36).toUpperCase();
|
||||
@@ -99,31 +89,32 @@ class HomeworkDetailHandler extends Handler {
|
||||
}
|
||||
this.response.template = 'homework_detail.html';
|
||||
this.response.body = {
|
||||
tdoc, tsdoc, attended, udict, pdict, psdict, rdict, ddocs, page, dpcount, dcount, path,
|
||||
tdoc, tsdoc, attended, udict, pdict, psdict, rdict, ddocs, page, dpcount, dcount,
|
||||
};
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
async postAttend(domainId: string, tid: ObjectID) {
|
||||
this.checkPerm(PERM.PERM_ATTEND_HOMEWORK);
|
||||
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
|
||||
const tdoc = await contest.get(domainId, tid);
|
||||
if (contest.isDone(tdoc)) throw new HomeworkNotLiveError(tdoc.docId);
|
||||
await contest.attend(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK);
|
||||
await contest.attend(domainId, tdoc.docId, this.user._id);
|
||||
this.back();
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
async postDelete(domainId: string, tid: ObjectID) {
|
||||
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
|
||||
const tdoc = await contest.get(domainId, tid);
|
||||
if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
|
||||
await contest.del(domainId, tid, document.TYPE_HOMEWORK);
|
||||
await contest.del(domainId, tid);
|
||||
this.response.redirect = this.url('homework_main');
|
||||
}
|
||||
}
|
||||
|
||||
class HomeworkDetailProblemHandler extends Handler {
|
||||
tdoc: Tdoc<60>;
|
||||
tdoc: Tdoc<30>;
|
||||
pdoc: ProblemDoc;
|
||||
pid: ProblemId;
|
||||
tsdoc: any;
|
||||
udoc: User;
|
||||
attended: boolean;
|
||||
@@ -131,19 +122,20 @@ class HomeworkDetailProblemHandler extends Handler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('pid', Types.String)
|
||||
async _prepare(domainId: string, tid: ObjectID, _pid: string) {
|
||||
this.tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
|
||||
const pid = this.tdoc.pids[parseInt(_pid, 36) - 10];
|
||||
if (!pid) throw new ProblemNotFoundError(domainId, tid, _pid);
|
||||
this.tdoc = await contest.get(domainId, tid);
|
||||
if (this.tdoc.rule !== 'homework') throw new ContestNotFoundError(domainId, tid);
|
||||
this.pid = this.tdoc.pids[parseInt(_pid, 36) - 10];
|
||||
if (!this.pid) throw new ProblemNotFoundError(domainId, tid, _pid);
|
||||
[this.udoc, this.pdoc, this.tsdoc] = await Promise.all([
|
||||
user.getById(domainId, this.tdoc.owner),
|
||||
problem.get(domainId, pid),
|
||||
contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK),
|
||||
problem.get(domainId, this.pid),
|
||||
contest.getStatus(domainId, tid, this.user._id),
|
||||
]);
|
||||
if (!this.pdoc) throw new ProblemNotFoundError(domainId, pid);
|
||||
if (!this.pdoc) throw new ProblemNotFoundError(domainId, this.pid);
|
||||
// @ts-ignore
|
||||
if (this.pdoc.domainId !== domainId) this.pdoc.docId = `${this.pdoc.domainId}:${this.pdoc.docId}`;
|
||||
this.pdoc.pid = _pid;
|
||||
this.attended = this.tsdoc && this.tsdoc.attend === 1;
|
||||
this.attended = this.tsdoc?.attend === 1;
|
||||
this.response.body = {
|
||||
tdoc: this.tdoc,
|
||||
pdoc: this.pdoc,
|
||||
@@ -212,13 +204,11 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
|
||||
pdomain: DomainDoc;
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('pid', Types.Name)
|
||||
async prepare(domainId: string, tid: ObjectID, _pid: string) {
|
||||
const pid = this.tdoc.pids[parseInt(_pid, 36) - 10];
|
||||
async prepare(domainId: string, tid: ObjectID) {
|
||||
if (!this.attended) throw new HomeworkNotAttendedError(tid);
|
||||
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
|
||||
this.pdomainId = typeof pid === 'string' ? pid.split(':')[0] : domainId;
|
||||
this.ppid = typeof pid === 'number' ? pid : +pid.split(':')[1];
|
||||
this.pdomainId = typeof this.pid === 'string' ? this.pid.split(':')[0] : domainId;
|
||||
this.ppid = typeof this.pid === 'number' ? this.pid : +this.pid.split(':')[1];
|
||||
this.pdomain = await domain.get(this.pdomainId);
|
||||
if (this.pdomain.langs) {
|
||||
this.response.body.pdoc.config.langs = intersection(
|
||||
@@ -243,29 +233,23 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
|
||||
}
|
||||
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('pid', Types.String)
|
||||
@param('code', Types.Content)
|
||||
@param('lang', Types.Name)
|
||||
@param('pretest', Types.Boolean)
|
||||
@param('input', Types.String, true)
|
||||
async post(domainId: string, tid: ObjectID, _pid: string, code: string, lang: string, pretest = false, input = '') {
|
||||
const pid = this.tdoc.pids[parseInt(_pid, 36) - 10];
|
||||
async post(domainId: string, tid: ObjectID, code: string, lang: string, pretest = false, input = '') {
|
||||
if (this.response.body.pdoc.config?.langs && !this.response.body.pdoc.config.langs.includes(lang)) {
|
||||
throw new BadRequestError('Language not allowed.');
|
||||
}
|
||||
await this.limitRate('add_record', 60, system.get('limit.submission'));
|
||||
const rid = await record.add(domainId, pid, this.user._id, lang, code, true, pretest ? input : {
|
||||
type: document.TYPE_HOMEWORK,
|
||||
tid,
|
||||
});
|
||||
const rid = await record.add(domainId, this.pid, this.user._id, lang, code, true, pretest ? input : tid);
|
||||
const rdoc = await record.get(domainId, rid);
|
||||
if (!pretest) {
|
||||
await Promise.all([
|
||||
(this.tsdoc.journal || []).filter((i) => i.pid === pid).length
|
||||
(this.tsdoc.journal || []).filter((i) => i.pid === this.pid).length
|
||||
&& problem.inc(this.pdomainId, this.ppid, 'nSubmit', 1),
|
||||
domain.incUserInDomain(domainId, this.user._id, 'nSubmit'),
|
||||
contest.updateStatus(domainId, tid, this.user._id,
|
||||
rid, pid, STATUS.STATUS_WAITING, 0, document.TYPE_HOMEWORK),
|
||||
contest.updateStatus(domainId, tid, this.user._id, rid, this.pid, STATUS.STATUS_WAITING),
|
||||
]);
|
||||
}
|
||||
bus.broadcast('record/change', rdoc);
|
||||
@@ -279,9 +263,7 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
|
||||
class HomeworkEditHandler extends Handler {
|
||||
@param('tid', Types.ObjectID, true)
|
||||
async get(domainId: string, tid: ObjectID) {
|
||||
const tdoc = tid
|
||||
? await contest.get(domainId, tid, document.TYPE_HOMEWORK)
|
||||
: null;
|
||||
const tdoc = tid ? await contest.get(domainId, tid) : null;
|
||||
if (!tid) this.checkPerm(PERM.PERM_CREATE_HOMEWORK);
|
||||
else if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
|
||||
else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF);
|
||||
@@ -290,16 +272,6 @@ class HomeworkEditHandler extends Handler {
|
||||
(tdoc.endAt.getTime() - tdoc.penaltySince.getTime()) / (Time.day / 100),
|
||||
) / 100
|
||||
: 1;
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
['homework_main', 'homework_main'],
|
||||
...tid
|
||||
? [
|
||||
[tdoc.title, 'homework_detail', { tid }, true],
|
||||
['homework_edit', null],
|
||||
]
|
||||
: [['homework_create', null]],
|
||||
];
|
||||
const beginAt = tid
|
||||
? moment(tdoc.beginAt).tz(this.user.timeZone)
|
||||
: moment().add(1, 'day').tz(this.user.timeZone).hour(0).minute(0).millisecond(0);
|
||||
@@ -316,7 +288,6 @@ class HomeworkEditHandler extends Handler {
|
||||
extensionDays,
|
||||
penaltyRules: tid ? yaml.dump(tdoc.penaltyRules) : null,
|
||||
pids: tid ? tdoc.pids.join(',') : '',
|
||||
path,
|
||||
page_name: tid ? 'homework_edit' : 'homework_create',
|
||||
};
|
||||
}
|
||||
@@ -344,9 +315,7 @@ class HomeworkEditHandler extends Handler {
|
||||
if (!i.includes(':')) throw new ValidationError('pids');
|
||||
return i;
|
||||
}).filter((i) => i);
|
||||
const tdoc = tid
|
||||
? await contest.get(domainId, tid, document.TYPE_HOMEWORK)
|
||||
: null;
|
||||
const tdoc = tid ? await contest.get(domainId, tid) : null;
|
||||
if (!tid) this.checkPerm(PERM.PERM_CREATE_HOMEWORK);
|
||||
else if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
|
||||
else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF);
|
||||
@@ -361,16 +330,16 @@ class HomeworkEditHandler extends Handler {
|
||||
if (!tid) {
|
||||
tid = await contest.add(domainId, title, content, this.user._id,
|
||||
'homework', beginAt.toDate(), endAt.toDate(), pids, rated,
|
||||
{ penaltySince: penaltySince.toDate(), penaltyRules }, document.TYPE_HOMEWORK);
|
||||
{ penaltySince: penaltySince.toDate(), penaltyRules });
|
||||
} else {
|
||||
await contest.edit(domainId, tid, {
|
||||
title, content, beginAt: beginAt.toDate(), endAt: endAt.toDate(), pids, penaltySince: penaltySince.toDate(), penaltyRules, rated,
|
||||
}, document.TYPE_HOMEWORK);
|
||||
});
|
||||
if (tdoc.beginAt !== beginAt.toDate()
|
||||
|| tdoc.endAt !== endAt.toDate()
|
||||
|| tdoc.penaltySince !== penaltySince.toDate()
|
||||
|| tdoc.pids.sort().join(' ') !== pids.sort().join(' ')) {
|
||||
await contest.recalcStatus(domainId, tdoc.docId, document.TYPE_HOMEWORK);
|
||||
await contest.recalcStatus(domainId, tdoc.docId);
|
||||
}
|
||||
}
|
||||
this.response.body = { tid };
|
||||
@@ -383,8 +352,7 @@ class HomeworkScoreboardHandler extends Handler {
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, tid: ObjectID, page = 1) {
|
||||
const [tdoc, rows, udict, , nPages] = await contest.getScoreboard.call(
|
||||
this, domainId, tid, false,
|
||||
page, document.TYPE_HOMEWORK,
|
||||
this, domainId, tid, false, page,
|
||||
);
|
||||
const path = [
|
||||
['Hydro', 'homepage'],
|
||||
@@ -409,7 +377,7 @@ class HomeworkScoreboardDownloadHandler extends Handler {
|
||||
html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }),
|
||||
};
|
||||
if (!getContent[ext]) throw new ValidationError('ext');
|
||||
const [tdoc, rows] = await contest.getScoreboard.call(this, domainId, tid, true, 0, document.TYPE_HOMEWORK);
|
||||
const [tdoc, rows] = await contest.getScoreboard.call(this, domainId, tid, true, 0);
|
||||
this.binary(await getContent[ext](rows), `${tdoc.title}.${ext}`);
|
||||
}
|
||||
}
|
||||
@@ -418,9 +386,7 @@ class HomeworkCodeHandler extends Handler {
|
||||
@param('tid', Types.ObjectID)
|
||||
async get(domainId: string, tid: ObjectID) {
|
||||
await this.limitRate('contest_code', 3600, 60);
|
||||
const [tdoc, tsdocs] = await contest.getAndListStatus(
|
||||
domainId, tid, document.TYPE_HOMEWORK,
|
||||
);
|
||||
const [tdoc, tsdocs] = await contest.getAndListStatus(domainId, tid);
|
||||
if (!this.user.own(tdoc) && !this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE)) {
|
||||
this.checkPerm(PERM.PERM_READ_RECORD_CODE);
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ export async function postJudge(rdoc: RecordDoc) {
|
||||
const updated = await problem.updateStatus(rdoc.pdomain, rdoc.pid, rdoc.uid, rdoc._id, rdoc.status, rdoc.score);
|
||||
if (rdoc.contest) {
|
||||
await contest.updateStatus(
|
||||
rdoc.domainId, rdoc.contest.tid, rdoc.uid, rdoc._id,
|
||||
rdoc.domainId, rdoc.contest, rdoc.uid, rdoc._id,
|
||||
rdoc.domainId === rdoc.pdomain ? rdoc.pid : `${rdoc.pdomain}:${rdoc.pid}`,
|
||||
rdoc.status, rdoc.score, rdoc.contest.type,
|
||||
rdoc.status, rdoc.score,
|
||||
);
|
||||
} else if (accept && updated) await domain.incUserInDomain(rdoc.domainId, rdoc.uid, 'nAccept', 1);
|
||||
const pdoc = (accept && updated)
|
||||
|
||||
@@ -16,7 +16,6 @@ import paginate from '../lib/paginate';
|
||||
import { isPid, parsePid as convertPid } from '../lib/validator';
|
||||
import { PERM, PRIV } from '../model/builtin';
|
||||
import * as contest from '../model/contest';
|
||||
import * as document from '../model/document';
|
||||
import domain from '../model/domain';
|
||||
import problem from '../model/problem';
|
||||
import record from '../model/record';
|
||||
@@ -258,12 +257,8 @@ export class ProblemDetailHandler extends ProblemHandler {
|
||||
|
||||
async postDelete() {
|
||||
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
|
||||
const [ctdocs, htdocs] = await Promise.all([
|
||||
contest.getRelated(this.domainId, this.pdoc.docId, document.TYPE_CONTEST),
|
||||
contest.getRelated(this.domainId, this.pdoc.docId, document.TYPE_HOMEWORK),
|
||||
]);
|
||||
if (ctdocs.length) throw new BadRequestError('Problem already used by contest {0}', ctdocs[0]._id);
|
||||
if (htdocs.length) throw new BadRequestError('Problem already used by homwrork {0}', htdocs[0]._id);
|
||||
const tdocs = await contest.getRelated(this.domainId, this.pdoc.docId);
|
||||
if (tdocs.length) throw new BadRequestError('Problem already used by contest {0}', tdocs[0]._id);
|
||||
await problem.del(this.pdoc.domainId, this.pdoc.docId);
|
||||
this.response.redirect = this.url('problem_main');
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
ContestNotAttendedError, ContestNotFoundError, PermissionError,
|
||||
ProblemNotFoundError, RecordNotFoundError, UserNotFoundError,
|
||||
} from '../error';
|
||||
import { RecordDoc } from '../interface';
|
||||
import { RecordDoc, Tdoc } from '../interface';
|
||||
import { PERM, PRIV, STATUS } from '../model/builtin';
|
||||
import * as contest from '../model/contest';
|
||||
import problem from '../model/problem';
|
||||
@@ -34,13 +34,13 @@ class RecordListHandler extends Handler {
|
||||
let tdoc = null;
|
||||
let invalid = false;
|
||||
this.response.template = 'record_main.html';
|
||||
const q: FilterQuery<RecordDoc> = { 'contest.tid': tid, hidden: false };
|
||||
const q: FilterQuery<RecordDoc> = { contest: tid };
|
||||
if (full) uidOrName = this.user._id.toString();
|
||||
if (tid) {
|
||||
tdoc = await contest.get(domainId, tid, -1);
|
||||
tdoc = await contest.get(domainId, tid);
|
||||
if (!tdoc) throw new ContestNotFoundError(domainId, pid);
|
||||
if (!contest.canShowScoreboard.call(this, tdoc, true)) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
||||
if (!this.user.own(tdoc) && !(await contest.getStatus(domainId, tid, this.user._id, -1))?.attend) {
|
||||
if (!this.user.own(tdoc) && !(await contest.getStatus(domainId, tid, this.user._id))?.attend) {
|
||||
throw new ContestNotAttendedError(domainId, tid);
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ class RecordDetailHandler extends Handler {
|
||||
const rdoc = await record.get(domainId, rid);
|
||||
if (!rdoc) throw new RecordNotFoundError(rid);
|
||||
if (rdoc.contest) {
|
||||
const tdoc = await contest.get(domainId, rdoc.contest.tid, rdoc.contest.type);
|
||||
const tdoc = await contest.get(domainId, rdoc.contest);
|
||||
let canView = this.user.own(tdoc);
|
||||
canView ||= contest.canShowRecord.call(this, tdoc);
|
||||
canView ||= contest.canShowSelfRecord.call(this, tdoc, true) && rdoc.uid === this.user._id;
|
||||
@@ -135,7 +135,7 @@ class RecordDetailHandler extends Handler {
|
||||
}
|
||||
this.response.body = { udoc, rdoc, pdoc };
|
||||
if (rdoc.contest) {
|
||||
this.response.body.tdoc = await contest.get(domainId, rdoc.contest.tid, rdoc.contest.type);
|
||||
this.response.body.tdoc = await contest.get(domainId, rdoc.contest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +182,7 @@ class RecordMainConnectionHandler extends ConnectionHandler {
|
||||
pid: number;
|
||||
status: number;
|
||||
pretest = false;
|
||||
tdoc: Tdoc<30>;
|
||||
|
||||
@param('tid', Types.ObjectID, true)
|
||||
@param('pid', Types.Name, true)
|
||||
@@ -194,9 +195,9 @@ class RecordMainConnectionHandler extends ConnectionHandler {
|
||||
status?: number, pretest = false, all = false,
|
||||
) {
|
||||
if (tid) {
|
||||
const tdoc = await contest.get(domainId, tid, -1);
|
||||
if (!tdoc) throw new ContestNotFoundError(domainId, tid);
|
||||
if (pretest || contest.canShowScoreboard.call(this, tdoc, true)) this.tid = tid.toHexString();
|
||||
this.tdoc = await contest.get(domainId, tid);
|
||||
if (!this.tdoc) throw new ContestNotFoundError(domainId, tid);
|
||||
if (pretest || contest.canShowScoreboard.call(this, this.tdoc, true)) this.tid = tid.toHexString();
|
||||
else throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
||||
}
|
||||
if (pretest) {
|
||||
@@ -236,10 +237,12 @@ class RecordMainConnectionHandler extends ConnectionHandler {
|
||||
}
|
||||
|
||||
async onRecordChange(rdoc: RecordDoc) {
|
||||
if (!this.all && !this.pretest && rdoc.input) return;
|
||||
if (!this.all && rdoc.domainId !== this.domainId) return;
|
||||
if (!this.all && rdoc.contest && rdoc.contest.tid.toString() !== this.tid) return;
|
||||
if (!this.all && this.pid && rdoc.pid !== this.pid) return;
|
||||
if (!this.all) {
|
||||
if (!this.pretest && rdoc.input) return;
|
||||
if (rdoc.domainId !== this.domainId) return;
|
||||
if (rdoc.contest && ![this.tid, '000000000000000000000000'].includes(rdoc.contest.toString())) return;
|
||||
}
|
||||
if (this.pid && rdoc.pid !== this.pid) return;
|
||||
if (this.uid && rdoc.uid !== this.uid) return;
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
@@ -247,7 +250,7 @@ class RecordMainConnectionHandler extends ConnectionHandler {
|
||||
user.getById(this.domainId, rdoc.uid),
|
||||
problem.get(rdoc.pdomain, rdoc.pid),
|
||||
]);
|
||||
const tdoc = this.tid ? await contest.get(rdoc.domainId, new ObjectID(this.tid), -1) : null;
|
||||
const tdoc = this.tid ? this.tdoc || await contest.get(rdoc.domainId, new ObjectID(this.tid)) : null;
|
||||
if (pdoc && !rdoc.contest) {
|
||||
if (pdoc.hidden && !this.user.own(pdoc) && !this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null;
|
||||
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM)) pdoc = null;
|
||||
@@ -272,7 +275,7 @@ class RecordDetailConnectionHandler extends ConnectionHandler {
|
||||
const rdoc = await record.get(domainId, rid);
|
||||
if (!rdoc) return;
|
||||
if (rdoc.contest && rdoc.input === undefined) {
|
||||
const tdoc = await contest.get(domainId, rdoc.contest.tid, -1);
|
||||
const tdoc = await contest.get(domainId, rdoc.contest);
|
||||
let canView = this.user.own(tdoc);
|
||||
canView ||= contest.canShowRecord.call(this, tdoc);
|
||||
canView ||= this.user._id === rdoc.uid && contest.canShowSelfRecord.call(this, tdoc);
|
||||
|
||||
@@ -28,7 +28,6 @@ export interface SystemKeys {
|
||||
'smtp.host': string,
|
||||
'smtp.port': number,
|
||||
'smtp.secure': boolean,
|
||||
'user': number,
|
||||
'installid': string,
|
||||
'server.name': string,
|
||||
'server.url': string,
|
||||
@@ -276,11 +275,6 @@ export interface TestCase {
|
||||
message: string,
|
||||
}
|
||||
|
||||
export interface ContestInfo {
|
||||
type: 30 | 60,
|
||||
tid: ObjectID,
|
||||
}
|
||||
|
||||
export interface RecordDoc {
|
||||
_id: ObjectID;
|
||||
domainId: string;
|
||||
@@ -301,11 +295,10 @@ export interface RecordDoc {
|
||||
judgeAt: Date;
|
||||
status: number;
|
||||
progress?: number;
|
||||
/** pretest/script */
|
||||
hidden?: boolean;
|
||||
/** pretest */
|
||||
input?: string;
|
||||
contest?: ContestInfo;
|
||||
/** 0 if pretest&script */
|
||||
contest?: ObjectID;
|
||||
}
|
||||
|
||||
export interface ScoreboardNode {
|
||||
@@ -329,7 +322,7 @@ export interface TrainingNode {
|
||||
export type ExternalProblemId = string; // ${string}:${number}
|
||||
export type ProblemId = ExternalProblemId | number;
|
||||
|
||||
export interface Tdoc<docType = document['TYPE_CONTEST'] | document['TYPE_HOMEWORK'] | document['TYPE_TRAINING']> extends Document {
|
||||
export interface Tdoc<docType = document['TYPE_CONTEST'] | document['TYPE_TRAINING']> extends Document {
|
||||
docId: ObjectID,
|
||||
docType: docType & number,
|
||||
beginAt: Date,
|
||||
@@ -461,15 +454,15 @@ export interface ContestRule {
|
||||
TEXT: string;
|
||||
check: (args: any) => any;
|
||||
statusSort: any;
|
||||
showScoreboard: (tdoc: Tdoc<30 | 60>, now: Date) => boolean;
|
||||
showSelfRecord: (tdoc: Tdoc<30 | 60>, now: Date) => boolean;
|
||||
showRecord: (tdoc: Tdoc<30 | 60>, now: Date) => boolean;
|
||||
stat: (tdoc: Tdoc<30 | 60>, journal: any[]) => ContestStat;
|
||||
showScoreboard: (tdoc: Tdoc<30>, now: Date) => boolean;
|
||||
showSelfRecord: (tdoc: Tdoc<30>, now: Date) => boolean;
|
||||
showRecord: (tdoc: Tdoc<30>, now: Date) => boolean;
|
||||
stat: (tdoc: Tdoc<30>, journal: any[]) => ContestStat;
|
||||
scoreboard: (
|
||||
isExport: boolean, _: (s: string) => string,
|
||||
tdoc: Tdoc<30 | 60>, pdict: ProblemDict, cursor: Cursor<any>, page: number,
|
||||
tdoc: Tdoc<30>, pdict: ProblemDict, cursor: Cursor<any>, page: number,
|
||||
) => Promise<[board: ScoreboardRow[], udict: Udict, nPages: number]>;
|
||||
ranked: (tdoc: Tdoc<30 | 60>, cursor: Cursor<any>) => Promise<any[]>;
|
||||
ranked: (tdoc: Tdoc<30>, cursor: Cursor<any>) => Promise<any[]>;
|
||||
}
|
||||
|
||||
export type ContestRules = Dictionary<ContestRule>;
|
||||
|
||||
@@ -58,5 +58,5 @@ export async function load() {
|
||||
|
||||
export async function loadCli() {
|
||||
await require('./entry/cli').load();
|
||||
process.kill(process.pid, 'SIGINT');
|
||||
setTimeout(() => process.exit(0), 300);
|
||||
}
|
||||
@@ -16,8 +16,6 @@ import * as document from './document';
|
||||
import problem from './problem';
|
||||
import user from './user';
|
||||
|
||||
type Type = 30 | 60;
|
||||
|
||||
interface AcmJournal {
|
||||
rid: ObjectID;
|
||||
pid: ProblemId;
|
||||
@@ -439,7 +437,7 @@ function _getStatusJournal(tsdoc) {
|
||||
export async function add(
|
||||
domainId: string, title: string, content: string, owner: number,
|
||||
rule: string, beginAt = new Date(), endAt = new Date(), pids: ProblemId[] = [],
|
||||
rated = false, data: Partial<Tdoc> = {}, type: Type = document.TYPE_CONTEST,
|
||||
rated = false, data: Partial<Tdoc> = {},
|
||||
) {
|
||||
if (!this.RULES[rule]) throw new ValidationError('rule');
|
||||
if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt');
|
||||
@@ -448,101 +446,79 @@ export async function add(
|
||||
});
|
||||
this.RULES[rule].check(data);
|
||||
await bus.serial('contest/before-add', data);
|
||||
const res = await document.add(domainId, content, owner, type, null, null, null, {
|
||||
const res = await document.add(domainId, content, owner, document.TYPE_CONTEST, null, null, null, {
|
||||
...data, title, rule, beginAt, endAt, pids, attend: 0, rated,
|
||||
});
|
||||
await bus.serial('contest/add', data, res);
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function edit(
|
||||
domainId: string, tid: ObjectID,
|
||||
$set: any, type: Type = document.TYPE_CONTEST,
|
||||
) {
|
||||
if ($set.rule) {
|
||||
if (!this.RULES[$set.rule]) throw new ValidationError('rule');
|
||||
}
|
||||
const tdoc = await document.get(domainId, type, tid);
|
||||
export async function edit(domainId: string, tid: ObjectID, $set: any) {
|
||||
if ($set.rule && !this.RULES[$set.rule]) throw new ValidationError('rule');
|
||||
const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid);
|
||||
if (!tdoc) throw new ContestNotFoundError(domainId, tid);
|
||||
this.RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set));
|
||||
return await document.set(domainId, type, tid, $set);
|
||||
return await document.set(domainId, document.TYPE_CONTEST, tid, $set);
|
||||
}
|
||||
|
||||
export async function del(domainId: string, tid: ObjectID, type: Type = document.TYPE_CONTEST) {
|
||||
export async function del(domainId: string, tid: ObjectID) {
|
||||
await Promise.all([
|
||||
document.deleteOne(domainId, type, tid),
|
||||
document.deleteMultiStatus(domainId, type, { docId: tid }),
|
||||
document.deleteMulti(domainId, document.TYPE_DISCUSSION, { parentType: type, parentId: tid }),
|
||||
document.deleteOne(domainId, document.TYPE_CONTEST, tid),
|
||||
document.deleteMultiStatus(domainId, document.TYPE_CONTEST, { docId: tid }),
|
||||
document.deleteMulti(domainId, document.TYPE_DISCUSSION, { parentType: document.TYPE_CONTEST, parentId: tid }),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function get(domainId: string, tid: ObjectID, type: -1): Promise<Tdoc<30 | 60>>;
|
||||
export async function get<T extends Type>(domainId: string, tid: ObjectID, type: T): Promise<Tdoc<T>>;
|
||||
export async function get(domainId: string, tid: ObjectID): Promise<Tdoc<30>>;
|
||||
export async function get(domainId: string, tid: ObjectID, type: Type | -1 = document.TYPE_CONTEST) {
|
||||
let tdoc: Tdoc;
|
||||
if (type === -1) {
|
||||
tdoc = await document.get(domainId, document.TYPE_CONTEST, tid);
|
||||
if (!tdoc) tdoc = await document.get(domainId, document.TYPE_HOMEWORK, tid);
|
||||
} else tdoc = await document.get(domainId, type, tid);
|
||||
export async function get(domainId: string, tid: ObjectID): Promise<Tdoc<30>> {
|
||||
const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid);
|
||||
if (!tdoc) throw new ContestNotFoundError(tid);
|
||||
return tdoc;
|
||||
}
|
||||
|
||||
export async function getRelated(domainId: string, pid: ProblemId, type: Type = document.TYPE_CONTEST) {
|
||||
return await document.getMulti(domainId, type, { pids: pid }).toArray();
|
||||
export async function getRelated(domainId: string, pid: ProblemId) {
|
||||
return await document.getMulti(domainId, document.TYPE_CONTEST, { pids: pid }).toArray();
|
||||
}
|
||||
|
||||
export function getStatus(
|
||||
domainId: string, tid: ObjectID, uid: number,
|
||||
type: 30 | 60 | -1 = document.TYPE_CONTEST,
|
||||
) {
|
||||
if (type === -1) type = { $in: [30, 60] } as any;
|
||||
return document.getStatus(domainId, type, tid, uid);
|
||||
export function getStatus(domainId: string, tid: ObjectID, uid: number) {
|
||||
return document.getStatus(domainId, document.TYPE_CONTEST, tid, uid);
|
||||
}
|
||||
|
||||
export async function updateStatus(
|
||||
domainId: string, tid: ObjectID, uid: number, rid: ObjectID, pid: ProblemId,
|
||||
status = STATUS.STATUS_WRONG_ANSWER, score = 0, type: 30 | 60 = document.TYPE_CONTEST,
|
||||
status = STATUS.STATUS_WRONG_ANSWER, score = 0,
|
||||
) {
|
||||
const [tdoc, otsdoc] = await Promise.all([
|
||||
get(domainId, tid, type),
|
||||
getStatus(domainId, tid, uid, type),
|
||||
get(domainId, tid),
|
||||
getStatus(domainId, tid, uid),
|
||||
]);
|
||||
if (!otsdoc.attend) throw new ContestNotAttendedError(tid, uid);
|
||||
const tsdoc = await document.revPushStatus(domainId, type, tid, uid, 'journal', {
|
||||
const tsdoc = await document.revPushStatus(domainId, document.TYPE_CONTEST, tid, uid, 'journal', {
|
||||
rid, pid, status, score,
|
||||
}, 'rid');
|
||||
const journal = _getStatusJournal(tsdoc);
|
||||
const stats = RULES[tdoc.rule].stat(tdoc, journal);
|
||||
return await document.revSetStatus(domainId, type, tid, uid, tsdoc.rev, { journal, ...stats });
|
||||
return await document.revSetStatus(domainId, document.TYPE_CONTEST, tid, uid, tsdoc.rev, { journal, ...stats });
|
||||
}
|
||||
|
||||
export async function getListStatus(
|
||||
domainId: string, uid: number, tids: ObjectID[],
|
||||
type: 30 | 60 = document.TYPE_CONTEST,
|
||||
) {
|
||||
export async function getListStatus(domainId: string, uid: number, tids: ObjectID[]) {
|
||||
const r = {};
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
for (const tid of tids) r[tid.toHexString()] = await getStatus(domainId, tid, uid, type);
|
||||
for (const tid of tids) r[tid.toHexString()] = await getStatus(domainId, tid, uid);
|
||||
return r;
|
||||
}
|
||||
|
||||
export async function attend(
|
||||
domainId: string, tid: ObjectID, uid: number,
|
||||
type: Type = document.TYPE_CONTEST,
|
||||
) {
|
||||
export async function attend(domainId: string, tid: ObjectID, uid: number) {
|
||||
try {
|
||||
await document.cappedIncStatus(domainId, type, tid, uid, 'attend', 1, 0, 1);
|
||||
await document.cappedIncStatus(domainId, document.TYPE_CONTEST, tid, uid, 'attend', 1, 0, 1);
|
||||
} catch (e) {
|
||||
throw new ContestAlreadyAttendedError(tid, uid);
|
||||
}
|
||||
await document.inc(domainId, type, tid, 'attend', 1);
|
||||
await document.inc(domainId, document.TYPE_CONTEST, tid, 'attend', 1);
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getMultiStatus(domainId: string, query: any, docType: 30 | 60 = document.TYPE_CONTEST) {
|
||||
return document.getMultiStatus(domainId, docType, query);
|
||||
export function getMultiStatus(domainId: string, query: any) {
|
||||
return document.getMultiStatus(domainId, document.TYPE_CONTEST, query);
|
||||
}
|
||||
|
||||
export function isNew(tdoc: Tdoc, days = 1) {
|
||||
@@ -579,30 +555,28 @@ export function setStatus(domainId: string, tid: ObjectID, uid: number, $set: an
|
||||
return document.setStatus(domainId, document.TYPE_CONTEST, tid, uid, $set);
|
||||
}
|
||||
|
||||
export function count(domainId: string, query: any, type: Type = document.TYPE_CONTEST) {
|
||||
return document.count(domainId, type, query);
|
||||
export function count(domainId: string, query: any) {
|
||||
return document.count(domainId, document.TYPE_CONTEST, query);
|
||||
}
|
||||
|
||||
export function getMulti<K extends Type & keyof document.DocType>(
|
||||
domainId: string, query: FilterQuery<document.DocType[K]> = {}, type?: K,
|
||||
export function getMulti(
|
||||
domainId: string, query: FilterQuery<document.DocType['30']> = {},
|
||||
) {
|
||||
return document.getMulti(domainId, type || document.TYPE_CONTEST, query).sort({ beginAt: -1 });
|
||||
return document.getMulti(domainId, document.TYPE_CONTEST, query).sort({ beginAt: -1 });
|
||||
}
|
||||
|
||||
export async function getAndListStatus(
|
||||
domainId: string, tid: ObjectID, docType: Type | -1 = document.TYPE_CONTEST,
|
||||
): Promise<[Tdoc, any[]]> {
|
||||
export async function getAndListStatus(domainId: string, tid: ObjectID): Promise<[Tdoc, any[]]> {
|
||||
// TODO(iceboy): projection, pagination.
|
||||
const tdoc = await get(domainId, tid, docType as any);
|
||||
const tsdocs = await document.getMultiStatus(domainId, docType, { docId: tid })
|
||||
const tdoc = await get(domainId, tid);
|
||||
const tsdocs = await document.getMultiStatus(domainId, document.TYPE_CONTEST, { docId: tid })
|
||||
.sort(RULES[tdoc.rule].statusSort).toArray();
|
||||
return [tdoc, tsdocs];
|
||||
}
|
||||
|
||||
export async function recalcStatus(domainId: string, tid: ObjectID, type: Type = document.TYPE_CONTEST) {
|
||||
export async function recalcStatus(domainId: string, tid: ObjectID) {
|
||||
const [tdoc, tsdocs] = await Promise.all([
|
||||
document.get(domainId, type, tid),
|
||||
document.getMultiStatus(domainId, type, { docId: tid }).toArray(),
|
||||
document.get(domainId, document.TYPE_CONTEST, tid),
|
||||
document.getMultiStatus(domainId, document.TYPE_CONTEST, { docId: tid }).toArray(),
|
||||
]);
|
||||
const tasks = [];
|
||||
for (const tsdoc of tsdocs || []) {
|
||||
@@ -611,7 +585,7 @@ export async function recalcStatus(domainId: string, tid: ObjectID, type: Type =
|
||||
const stats = RULES[tdoc.rule].stat(tdoc, journal);
|
||||
tasks.push(
|
||||
document.revSetStatus(
|
||||
domainId, type, tid,
|
||||
domainId, document.TYPE_CONTEST, tid,
|
||||
tsdoc.uid, tsdoc.rev, { journal, ...stats },
|
||||
),
|
||||
);
|
||||
@@ -624,19 +598,19 @@ export function canViewHiddenScoreboard() {
|
||||
return this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
||||
}
|
||||
|
||||
export function canShowRecord(tdoc: Tdoc<30 | 60>, allowPermOverride = true) {
|
||||
export function canShowRecord(tdoc: Tdoc<30>, allowPermOverride = true) {
|
||||
if (RULES[tdoc.rule].showRecord(tdoc, new Date())) return true;
|
||||
if (allowPermOverride && canViewHiddenScoreboard.call(this)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function canShowSelfRecord(tdoc: Tdoc<30 | 60>, allowPermOverride = true) {
|
||||
export function canShowSelfRecord(tdoc: Tdoc<30>, allowPermOverride = true) {
|
||||
if (RULES[tdoc.rule].showSelfRecord(tdoc, new Date())) return true;
|
||||
if (allowPermOverride && canViewHiddenScoreboard.call(this)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function canShowScoreboard(tdoc: Tdoc<30 | 60>, allowPermOverride = true) {
|
||||
export function canShowScoreboard(tdoc: Tdoc<30>, allowPermOverride = true) {
|
||||
if (RULES[tdoc.rule].showScoreboard(tdoc, new Date())) return true;
|
||||
if (allowPermOverride && canViewHiddenScoreboard.call(this)) return true;
|
||||
return false;
|
||||
@@ -644,11 +618,11 @@ export function canShowScoreboard(tdoc: Tdoc<30 | 60>, allowPermOverride = true)
|
||||
|
||||
export async function getScoreboard(
|
||||
domainId: string, tid: ObjectID,
|
||||
isExport = false, page: number, docType: 30 | 60 = document.TYPE_CONTEST,
|
||||
isExport = false, page: number,
|
||||
): Promise<[Tdoc<30 | 60>, ScoreboardRow[], Udict, ProblemDict, number]> {
|
||||
const tdoc = await get(domainId, tid, docType);
|
||||
const tdoc = await get(domainId, tid);
|
||||
if (!canShowScoreboard.call(this, tdoc)) throw new ContestScoreboardHiddenError(tid);
|
||||
const tsdocsCursor = getMultiStatus(domainId, { docId: tid }, docType).sort(RULES[tdoc.rule].statusSort);
|
||||
const tsdocsCursor = getMultiStatus(domainId, { docId: tid }).sort(RULES[tdoc.rule].statusSort);
|
||||
const pdict = await problem.getList(domainId, tdoc.pids, true);
|
||||
const [rows, udict, nPages] = await RULES[tdoc.rule].scoreboard(
|
||||
isExport, this.translate.bind(this),
|
||||
|
||||
@@ -252,7 +252,7 @@ export async function getVnode(domainId: string, type: number, id: string, uid?:
|
||||
if ([document.TYPE_CONTEST, document.TYPE_TRAINING, document.TYPE_HOMEWORK].includes(type as any)) {
|
||||
const model = type === document.TYPE_TRAINING ? training : contest;
|
||||
const _id = new ObjectID(id);
|
||||
const tdoc = await model.get(domainId, _id, type as any);
|
||||
const tdoc = await model.get(domainId, _id);
|
||||
if (!tdoc) throw new DiscussionNodeNotFoundError(id);
|
||||
if (uid) {
|
||||
const tsdoc = await model.getStatus(domainId, _id, uid);
|
||||
|
||||
@@ -35,7 +35,6 @@ export interface DocType {
|
||||
[TYPE_DISCUSSION_REPLY]: DiscussionReplyDoc,
|
||||
[TYPE_CONTEST]: Tdoc,
|
||||
[TYPE_TRAINING]: TrainingDoc,
|
||||
[TYPE_HOMEWORK]: Tdoc,
|
||||
}
|
||||
|
||||
export interface DocStatusType {
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
} from 'mongodb';
|
||||
import { ProblemNotFoundError } from '../error';
|
||||
import {
|
||||
ContestInfo, ExternalProblemId, FileInfo,
|
||||
ProblemConfigFile, RecordDoc,
|
||||
ExternalProblemId, FileInfo, ProblemConfigFile,
|
||||
RecordDoc,
|
||||
} from '../interface';
|
||||
import * as bus from '../service/bus';
|
||||
import db from '../service/db';
|
||||
@@ -23,9 +23,8 @@ class RecordModel {
|
||||
static coll: Collection<RecordDoc> = db.collection('record');
|
||||
static PROJECTION_LIST: (keyof RecordDoc)[] = [
|
||||
'_id', 'score', 'time', 'memory', 'lang',
|
||||
'uid', 'pid', 'rejudged', 'hidden', 'progress',
|
||||
'uid', 'pid', 'rejudged', 'progress', 'pdomain',
|
||||
'contest', 'judger', 'judgeAt', 'status', 'domainId',
|
||||
'pdomain',
|
||||
];
|
||||
|
||||
static async submissionPriority(uid: number, base: number = 0) {
|
||||
@@ -98,7 +97,7 @@ class RecordModel {
|
||||
|
||||
static async add(
|
||||
domainId: string, pid: ExternalProblemId | number, uid: number,
|
||||
lang: string, code: string, addTask: boolean, contestOrConfig?: ContestInfo | string,
|
||||
lang: string, code: string, addTask: boolean, contestOrConfig?: ObjectID | string,
|
||||
) {
|
||||
let pdomain = domainId;
|
||||
if (typeof pid === 'string') {
|
||||
@@ -117,7 +116,6 @@ class RecordModel {
|
||||
score: 0,
|
||||
time: 0,
|
||||
memory: 0,
|
||||
hidden: false,
|
||||
judgeTexts: [],
|
||||
compilerTexts: [],
|
||||
testCases: [],
|
||||
@@ -129,7 +127,7 @@ class RecordModel {
|
||||
if (typeof contestOrConfig === 'string') {
|
||||
// is Run
|
||||
data.input = contestOrConfig;
|
||||
data.hidden = true;
|
||||
data.contest = new ObjectID('000000000000000000000000');
|
||||
} else {
|
||||
data.contest = contestOrConfig;
|
||||
isContest = true;
|
||||
@@ -207,16 +205,14 @@ class RecordModel {
|
||||
}
|
||||
|
||||
static async getList(
|
||||
domainId: string, rids: ObjectID[], showHidden: boolean, fields?: (keyof RecordDoc)[],
|
||||
domainId: string, rids: ObjectID[], fields?: (keyof RecordDoc)[],
|
||||
): Promise<Record<string, Partial<RecordDoc>>> {
|
||||
const r: Record<string, RecordDoc> = {};
|
||||
rids = Array.from(new Set(rids));
|
||||
let cursor = RecordModel.coll.find({ domainId, _id: { $in: rids } });
|
||||
if (fields) cursor = cursor.project(buildProjection(fields));
|
||||
const rdocs = await cursor.toArray();
|
||||
for (const rdoc of rdocs) {
|
||||
if (!rdoc.hidden || showHidden) r[rdoc._id.toHexString()] = rdoc;
|
||||
}
|
||||
for (const rdoc of rdocs) r[rdoc._id.toHexString()] = rdoc;
|
||||
return r;
|
||||
}
|
||||
}
|
||||
@@ -230,9 +226,9 @@ bus.on('domain/delete', (domainId) => Promise.all([
|
||||
|
||||
bus.once('app/started', () => db.ensureIndexes(
|
||||
RecordModel.coll,
|
||||
{ key: { domainId: 1, 'contest.tid': 1, hidden: 1, _id: -1 }, name: 'basic' },
|
||||
{ key: { domainId: 1, 'contest.tid': 1, hidden: 1, uid: 1, _id: -1 }, name: 'withUser' },
|
||||
{ key: { domainId: 1, 'contest.tid': 1, hidden: 1, pid: 1, _id: -1 }, name: 'withProblem' },
|
||||
{ key: { domainId: 1, contest: 1, _id: -1 }, name: 'basic' },
|
||||
{ key: { domainId: 1, contest: 1, uid: 1, _id: -1 }, name: 'withUser' },
|
||||
{ key: { domainId: 1, contest: 1, pid: 1, _id: -1 }, name: 'withProblem' },
|
||||
));
|
||||
|
||||
export default RecordModel;
|
||||
|
||||
@@ -103,6 +103,11 @@ async function cleanFiles() {
|
||||
}
|
||||
TaskModel.Worker.addHandler('storage.prune', cleanFiles);
|
||||
bus.once('app/started', async () => {
|
||||
if (process.env.NODE_APP_INSTANCE !== '0') return;
|
||||
await db.ensureIndexes(
|
||||
StorageModel.coll,
|
||||
{ key: { path: 1, autoDelete: 1 }, sparse: true, name: 'autoDelete' },
|
||||
);
|
||||
if (!await TaskModel.count({ type: 'schedule', subType: 'storage.prune' })) {
|
||||
await TaskModel.add({
|
||||
type: 'schedule',
|
||||
|
||||
@@ -137,6 +137,7 @@ class TaskModel {
|
||||
|
||||
const id = hostname();
|
||||
Worker.addHandler('task.daily', async () => {
|
||||
await global.Hydro.model.record.coll.deleteMany({ contest: new ObjectID('000000000000000000000000') });
|
||||
await global.Hydro.script.rp?.run({}, new Logger('task/rp').debug);
|
||||
await global.Hydro.script.problemStat?.run({}, new Logger('task/problem').debug);
|
||||
});
|
||||
|
||||
@@ -286,7 +286,7 @@ class UserModel {
|
||||
@ArgMethod
|
||||
static async ensureVuser(uname: string) {
|
||||
const [[min], current] = await Promise.all([
|
||||
collV.find({}).sort({ _id: -1 }).limit(1).toArray(),
|
||||
collV.find({}).sort({ _id: 1 }).limit(1).toArray(),
|
||||
collV.findOne({ unameLower: uname.toLowerCase() }),
|
||||
]);
|
||||
if (current) return current._id;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* eslint-disable no-constant-condition */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { ObjectID } from 'mongodb';
|
||||
import { STATUS } from '../model/builtin';
|
||||
import * as document from '../model/document';
|
||||
import db from '../service/db';
|
||||
@@ -6,11 +8,12 @@ import db from '../service/db';
|
||||
export const description = 'Recalculates nSubmit and nAccept in problem status.';
|
||||
|
||||
const sumStatus = (status) => ({ $sum: { $cond: [{ $eq: ['$status', status] }, 1, 0] } });
|
||||
const $match = { contest: { $ne: new ObjectID('000000000000000000000000') } };
|
||||
|
||||
export async function udoc(report) {
|
||||
report({ message: 'Udoc' });
|
||||
const pipeline = [
|
||||
{ $match: { hidden: false, type: { $ne: 'run' } } },
|
||||
{ $match },
|
||||
{
|
||||
$group: {
|
||||
_id: { domainId: '$domainId', pid: '$pid', uid: '$uid' },
|
||||
@@ -28,8 +31,9 @@ export async function udoc(report) {
|
||||
];
|
||||
let bulk = db.collection('domain.user').initializeUnorderedBulkOp();
|
||||
const cursor = db.collection('record').aggregate(pipeline, { allowDiskUse: true });
|
||||
while (await cursor.hasNext()) {
|
||||
while (true) {
|
||||
const adoc = await cursor.next() as any;
|
||||
if (!adoc) break;
|
||||
bulk.find({
|
||||
domainId: adoc._id.domainId,
|
||||
uid: adoc._id.uid,
|
||||
@@ -50,7 +54,7 @@ export async function udoc(report) {
|
||||
export async function psdoc(report) {
|
||||
report({ message: 'Psdoc' });
|
||||
const pipeline = [
|
||||
{ $match: { hidden: false, type: { $ne: 'run' } } },
|
||||
{ $match },
|
||||
{
|
||||
$group: {
|
||||
_id: { domainId: '$domainId', pid: '$pid', uid: '$uid' },
|
||||
@@ -59,8 +63,9 @@ export async function psdoc(report) {
|
||||
},
|
||||
];
|
||||
const data = db.collection('record').aggregate(pipeline, { allowDiskUse: true });
|
||||
while (await data.hasNext()) {
|
||||
while (true) {
|
||||
const adoc = await data.next() as any;
|
||||
if (!adoc) break;
|
||||
await document.setStatus(adoc._id.domainId, document.TYPE_PROBLEM, adoc._id.pid, adoc._id.uid, { nSubmit: adoc.nSubmit });
|
||||
}
|
||||
}
|
||||
@@ -68,7 +73,7 @@ export async function psdoc(report) {
|
||||
export async function pdoc(report) {
|
||||
report({ message: 'Pdoc' });
|
||||
const pipeline = [
|
||||
{ $match: { hidden: false, type: { $ne: 'run' } } },
|
||||
{ $match },
|
||||
{
|
||||
$group: {
|
||||
_id: { domainId: '$domainId', pid: '$pid', uid: '$uid' },
|
||||
@@ -106,8 +111,9 @@ export async function pdoc(report) {
|
||||
let bulk = db.collection('document').initializeUnorderedBulkOp();
|
||||
const data = db.collection('record').aggregate(pipeline, { allowDiskUse: true });
|
||||
let cnt = 0;
|
||||
while (await data.hasNext()) {
|
||||
while (true) {
|
||||
const adoc = await data.next() as any;
|
||||
if (!adoc) break;
|
||||
const $set = {
|
||||
nSubmit: adoc.nSubmit,
|
||||
nAccept: adoc.nAccept,
|
||||
|
||||
@@ -32,7 +32,7 @@ async function runProblem(...arg: any[]) {
|
||||
const psdocs = await problem.getMultiStatus(
|
||||
pdoc.domainId, { docId: pdoc.docId, rid: { $ne: null } },
|
||||
).limit(100).skip((page - 1) * 100).project({ rid: 1, uid: 1 }).toArray();
|
||||
const rdict = await record.getList(pdoc.domainId, psdocs.map((psdoc) => psdoc.rid), true);
|
||||
const rdict = await record.getList(pdoc.domainId, psdocs.map((psdoc) => psdoc.rid));
|
||||
for (const psdoc of psdocs) {
|
||||
if (rdict[psdoc.rid.toHexString()]) {
|
||||
const rp = rdict[psdoc.rid.toHexString()].score * p;
|
||||
@@ -48,12 +48,12 @@ async function runContest(
|
||||
): Promise<void>;
|
||||
async function runContest(...arg: any[]) {
|
||||
const start = new Date().getTime();
|
||||
const tdoc: Tdoc<30 | 60> = (typeof arg[0] === 'string')
|
||||
? await contest.get(arg[0], arg[1], -1)
|
||||
const tdoc: Tdoc<30> = (typeof arg[0] === 'string')
|
||||
? await contest.get(arg[0], arg[1])
|
||||
: arg[0];
|
||||
const udict: ND = (typeof arg[0] === 'string') ? arg[2] : arg[1];
|
||||
const report = (typeof arg[0] === 'string') ? arg[3] : arg[2];
|
||||
const cursor = contest.getMultiStatus(tdoc.domainId, tdoc.docId, tdoc.docType)
|
||||
const cursor = contest.getMultiStatus(tdoc.domainId, tdoc.docId)
|
||||
.sort(contest.RULES[tdoc.rule].statusSort);
|
||||
if (!await cursor.count()) return;
|
||||
const rankedTsdocs = await contest.RULES[tdoc.rule].ranked(tdoc, cursor);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import assert from 'assert';
|
||||
import { Readable } from 'stream';
|
||||
import { URL } from 'url';
|
||||
import { createReadStream } from 'fs-extra';
|
||||
import { BucketItem, Client, ItemBucketMetadata } from 'minio';
|
||||
import { Logger } from '../logger';
|
||||
import * as system from '../model/system';
|
||||
import { streamToBuffer } from '../utils';
|
||||
|
||||
const logger = new Logger('storage');
|
||||
|
||||
@@ -116,19 +114,13 @@ class StorageService {
|
||||
user: parseAlternativeEndpointUrl(this.opts.endPointForUser),
|
||||
judge: parseAlternativeEndpointUrl(this.opts.endPointForJudge),
|
||||
};
|
||||
await this.put('storage.test', Buffer.from('test'));
|
||||
const result = await streamToBuffer(await this.get('storage.test'));
|
||||
assert(result.toString() === 'test');
|
||||
await this.del('storage.test');
|
||||
logger.success('Storage connected.');
|
||||
this.error = null;
|
||||
} catch (e) {
|
||||
logger.warn('Storage init fail. will retry later.');
|
||||
if (process.env.DEV) logger.warn(e);
|
||||
this.error = e.toString();
|
||||
setTimeout(async () => {
|
||||
await this.start();
|
||||
}, 10000);
|
||||
setTimeout(() => this.start(), 10000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ const scripts: UpgradeScript[] = [
|
||||
// Init
|
||||
async function _1_2() {
|
||||
const ddoc = await domain.get('system');
|
||||
if (!ddoc) await domain.add('system', 1, 'Hydro', 'Hydro System');
|
||||
if (!ddoc) await domain.add('system', 1, 'Hydro', 'Welcome to Hydro!');
|
||||
// TODO discussion node?
|
||||
return true;
|
||||
},
|
||||
@@ -451,7 +451,6 @@ const scripts: UpgradeScript[] = [
|
||||
const _FRESH_INSTALL_IGNORE = 1;
|
||||
// Ignore drop index failure
|
||||
await db.collection('storage').dropIndex('path_1').catch(() => { });
|
||||
await db.collection('storage').createIndex({ path: 1, autoDelete: 1 }, { sparse: true });
|
||||
await db.collection('storage').updateMany(
|
||||
{ autoDelete: { $gte: moment().add(5, 'days').toDate() } },
|
||||
{ $unset: { autoDelete: '' } },
|
||||
@@ -524,7 +523,7 @@ const scripts: UpgradeScript[] = [
|
||||
await iterateAllDomain(async (ddoc) => {
|
||||
const ddocs = await discussion.getMulti(ddoc._id, {
|
||||
docType: document.TYPE_DISCUSSION,
|
||||
parentType: { $in: [document.TYPE_CONTEST, document.TYPE_HOMEWORK, document.TYPE_TRAINING] },
|
||||
parentType: { $in: [document.TYPE_CONTEST, 60, document.TYPE_TRAINING] },
|
||||
parentId: { $type: 'string' },
|
||||
}).toArray();
|
||||
for (const doc of ddocs) {
|
||||
@@ -535,6 +534,18 @@ const scripts: UpgradeScript[] = [
|
||||
});
|
||||
return true;
|
||||
},
|
||||
null,
|
||||
async function _46_47() {
|
||||
const _FRESH_INSTALL_IGNORE = 1;
|
||||
await document.coll.updateMany({ docType: document.TYPE_HOMEWORK }, { $set: { docType: document.TYPE_CONTEST } });
|
||||
await document.collStatus.updateMany({ docType: document.TYPE_HOMEWORK }, { $set: { docType: document.TYPE_CONTEST } });
|
||||
await RecordModel.coll.deleteMany({ 'contest.tid': { $ne: null }, hidden: true });
|
||||
await RecordModel.coll.updateMany({}, { $unset: { hidden: '' } });
|
||||
await RecordModel.coll.updateMany({ 'contest.tid': { $exists: true } }, { $rename: { 'contest.tid': 'contest1' } });
|
||||
await RecordModel.coll.updateMany({ contest1: { $exists: true } }, { $rename: { contest1: 'contest' } });
|
||||
await RecordModel.coll.updateMany({ contest: { $exists: true } }, { $unset: { contest: '' } });
|
||||
return true;
|
||||
},
|
||||
];
|
||||
|
||||
export default scripts;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/migrate-vijos",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.1",
|
||||
"main": "package.json",
|
||||
"repository": "git@github.com:hydro-dev/Hydro.git",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from 'hydrooj';
|
||||
|
||||
const dst = global.Hydro.service.db;
|
||||
const { discussion, document } = global.Hydro.model;
|
||||
const { discussion } = global.Hydro.model;
|
||||
const map = {};
|
||||
|
||||
const pid = (id) => {
|
||||
@@ -201,6 +201,7 @@ const tasks = {
|
||||
userfile_usage: null,
|
||||
},
|
||||
record: async (doc) => {
|
||||
if (doc.hidden) return null;
|
||||
const testCases: TestCase[] = [];
|
||||
for (const c of doc.cases || []) {
|
||||
testCases.push({
|
||||
@@ -227,16 +228,10 @@ const tasks = {
|
||||
judgeTexts: doc.judge_texts || [],
|
||||
compilerTexts: doc.compiler_texts || [],
|
||||
testCases,
|
||||
hidden: !!doc.hidden,
|
||||
rejudged: !!doc.rejudged,
|
||||
};
|
||||
if (doc.rejudged) rdoc.rejudged = true;
|
||||
if (doc.tid) {
|
||||
rdoc.contest = {
|
||||
type: doc.ttype || document.TYPE_CONTEST,
|
||||
tid: doc.tid,
|
||||
};
|
||||
}
|
||||
if (doc.tid) rdoc.contest = doc.tid;
|
||||
return rdoc;
|
||||
},
|
||||
domain: {
|
||||
|
||||
Reference in new issue
Block a user