core: performance: userCache&scoreboard.pagination

This commit is contained in:
undefined committed 2021-05-02 18:44:15 +08:00
1 parent 0454acdbf9
commit 0d7c53b927
12 files changed
+244 -166

No files matched your search

+2
View File
@@ -26,6 +26,7 @@
"koa-router": "^10.0.0", "koa-router": "^10.0.0",
"koa-static-cache": "^5.1.3", "koa-static-cache": "^5.1.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lru-cache": "^6.0.0",
"minio": "7.0.17", "minio": "7.0.17",
"moment-timezone": "^0.5.32", "moment-timezone": "^0.5.32",
"mongodb": "^3.6.6", "mongodb": "^3.6.6",
@@ -47,6 +48,7 @@
"@types/koa-router": "^7.4.1", "@types/koa-router": "^7.4.1",
"@types/koa-static-cache": "^5.1.0", "@types/koa-static-cache": "^5.1.0",
"@types/lodash": "^4.14.168", "@types/lodash": "^4.14.168",
"@types/lru-cache": "^5.1.0",
"@types/minio": "^7.0.7", "@types/minio": "^7.0.7",
"@types/mongodb": "^3.6.12", "@types/mongodb": "^3.6.12",
"@types/nodemailer": "^6.4.1", "@types/nodemailer": "^6.4.1",
+28 -28
View File
@@ -6,6 +6,7 @@ import {
ContestNotLiveError, ValidationError, ProblemNotFoundError, ContestNotLiveError, ValidationError, ProblemNotFoundError,
ContestNotAttendedError, PermissionError, ContestNotAttendedError, PermissionError,
} from '../error'; } from '../error';
import { Pdoc, Tdoc, User } from '../interface';
import paginate from '../lib/paginate'; import paginate from '../lib/paginate';
import { PERM, PRIV } from '../model/builtin'; import { PERM, PRIV } from '../model/builtin';
import * as contest from '../model/contest'; import * as contest from '../model/contest';
@@ -22,9 +23,7 @@ import {
import * as bus from '../service/bus'; import * as bus from '../service/bus';
import storage from '../service/storage'; import storage from '../service/storage';
const ContestHandler = contest.ContestHandlerMixin(Handler); class ContestListHandler extends Handler {
class ContestListHandler extends ContestHandler {
@param('rule', Types.Range(contest.RULES), true) @param('rule', Types.Range(contest.RULES), true)
@param('page', Types.PositiveInt, true) @param('page', Types.PositiveInt, true)
async get(domainId: string, rule = '', page = 1) { async get(domainId: string, rule = '', page = 1) {
@@ -54,7 +53,7 @@ class ContestListHandler extends ContestHandler {
} }
} }
class ContestDetailHandler extends ContestHandler { class ContestDetailHandler extends Handler {
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@param('page', Types.PositiveInt, true) @param('page', Types.PositiveInt, true)
async get(domainId: string, tid: ObjectID, page = 1) { async get(domainId: string, tid: ObjectID, page = 1) {
@@ -70,7 +69,7 @@ class ContestDetailHandler extends ContestHandler {
if (tsdoc) { if (tsdoc) {
attended = tsdoc.attend === 1; attended = tsdoc.attend === 1;
for (const pdetail of tsdoc.journal || []) psdict[pdetail.pid] = pdetail; for (const pdetail of tsdoc.journal || []) psdict[pdetail.pid] = pdetail;
if (this.canShowRecord(tdoc)) { if (contest.canShowRecord.call(this, tdoc)) {
const q = []; const q = [];
for (const i in psdict) q.push(psdict[i].rid); for (const i in psdict) q.push(psdict[i].rid);
rdict = await record.getList(domainId, q, true); rdict = await record.getList(domainId, q, true);
@@ -106,7 +105,7 @@ class ContestDetailHandler extends ContestHandler {
} }
} }
class ContestBoardcastHandler extends ContestHandler { class ContestBoardcastHandler extends Handler {
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) { async get(domainId: string, tid: ObjectID) {
const tdoc = await contest.get(domainId, tid); const tdoc = await contest.get(domainId, tid);
@@ -135,10 +134,11 @@ class ContestBoardcastHandler extends ContestHandler {
} }
} }
class ContestScoreboardHandler extends ContestHandler { class ContestScoreboardHandler extends Handler {
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) { @param('page', Types.PositiveInt)
const [tdoc, rows, udict] = await this.getScoreboard(domainId, tid); async get(domainId: string, tid: ObjectID, page = 1) {
const [tdoc, rows, udict] = await contest.getScoreboard(domainId, tid, false, page);
const path = [ const path = [
['Hydro', 'homepage'], ['Hydro', 'homepage'],
['contest_main', 'contest_main'], ['contest_main', 'contest_main'],
@@ -152,14 +152,7 @@ class ContestScoreboardHandler extends ContestHandler {
} }
} }
class ContestScoreboardRawHandler extends ContestHandler { class ContestScoreboardDownloadHandler extends Handler {
@param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) {
this.response.body = await this.getRawStatus(domainId, tid);
}
}
class ContestScoreboardDownloadHandler extends ContestHandler {
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@param('ext', Types.Range(['csv', 'html'])) @param('ext', Types.Range(['csv', 'html']))
async get(domainId: string, tid: ObjectID, ext: string) { async get(domainId: string, tid: ObjectID, ext: string) {
@@ -167,12 +160,14 @@ class ContestScoreboardDownloadHandler extends ContestHandler {
csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`, 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 }), html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }),
}; };
const [tdoc, rows] = await this.getScoreboard(domainId, tid, true); const [tdoc, rows] = await contest.getScoreboard(domainId, tid, true, 0);
this.binary(await getContent[ext](rows), `${tdoc.title}.${ext}`); this.binary(await getContent[ext](rows), `${tdoc.title}.${ext}`);
} }
} }
class ContestEditHandler extends ContestHandler { class ContestEditHandler extends Handler {
tdoc: Tdoc;
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
async prepare(domainId: string, tid: ObjectID) { async prepare(domainId: string, tid: ObjectID) {
this.tdoc = await contest.get(domainId, tid); this.tdoc = await contest.get(domainId, tid);
@@ -229,7 +224,7 @@ class ContestEditHandler extends ContestHandler {
const endAt = beginAtMoment.clone().add(duration, 'hours').toDate(); const endAt = beginAtMoment.clone().add(duration, 'hours').toDate();
if (beginAtMoment.isSameOrAfter(endAt)) throw new ValidationError('duration'); if (beginAtMoment.isSameOrAfter(endAt)) throw new ValidationError('duration');
const beginAt = beginAtMoment.toDate(); const beginAt = beginAtMoment.toDate();
pids = await this.verifyProblems(domainId, pids); pids = await contest.verifyProblems(domainId, pids);
await contest.edit(domainId, this.tdoc.docId, { await contest.edit(domainId, this.tdoc.docId, {
title, content, rule, beginAt, endAt, pids, rated, title, content, rule, beginAt, endAt, pids, rated,
}); });
@@ -242,7 +237,13 @@ class ContestEditHandler extends ContestHandler {
} }
} }
class ContestProblemHandler extends ContestHandler { class ContestProblemHandler extends Handler {
tdoc: Tdoc<30 | 60>;
pdoc: Pdoc;
tsdoc: any;
udoc: User;
attended: boolean;
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt) @param('pid', Types.UnsignedInt)
async _prepare(domainId: string, tid: ObjectID, pid: number) { async _prepare(domainId: string, tid: ObjectID, pid: number) {
@@ -308,7 +309,7 @@ class ContestDetailProblemSubmitHandler extends ContestProblemHandler {
async prepare(domainId: string, tid: ObjectID, pid: number) { async prepare(domainId: string, tid: ObjectID, pid: number) {
if (!contest.isOngoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc.docId); if (!contest.isOngoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc.docId);
let rdocs = []; let rdocs = [];
if (this.canShowRecord(this.tdoc)) { if (contest.canShowRecord.call(this, this.tdoc)) {
rdocs = await record.getUserInProblemMulti( rdocs = await record.getUserInProblemMulti(
domainId, this.user._id, domainId, this.user._id,
this.pdoc.docId, true, this.pdoc.docId, true,
@@ -326,7 +327,7 @@ class ContestDetailProblemSubmitHandler extends ContestProblemHandler {
tdoc: this.tdoc, tdoc: this.tdoc,
pdoc: this.pdoc, pdoc: this.pdoc,
udoc: this.udoc, udoc: this.udoc,
attended: this.attend, attended: this.attended,
path, path,
rdocs, rdocs,
page_name: 'contest_detail_problem_submit', page_name: 'contest_detail_problem_submit',
@@ -349,7 +350,7 @@ class ContestDetailProblemSubmitHandler extends ContestProblemHandler {
contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId), contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId),
]); ]);
bus.boardcast('record/change', rdoc); bus.boardcast('record/change', rdoc);
if (!this.canShowRecord(this.tdoc)) { if (!contest.canShowRecord.call(this, this.tdoc)) {
this.response.body = { tid }; this.response.body = { tid };
this.response.redirect = this.url('contest_detail', { tid }); this.response.redirect = this.url('contest_detail', { tid });
} else { } else {
@@ -359,7 +360,7 @@ class ContestDetailProblemSubmitHandler extends ContestProblemHandler {
} }
} }
class ContestCodeHandler extends ContestHandler { class ContestCodeHandler extends Handler {
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) { async get(domainId: string, tid: ObjectID) {
if (!this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE)) { if (!this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE)) {
@@ -386,7 +387,7 @@ class ContestCodeHandler extends ContestHandler {
} }
} }
class ContestCreateHandler extends ContestHandler { class ContestCreateHandler extends Handler {
async get() { async get() {
this.response.template = 'contest_edit.html'; this.response.template = 'contest_edit.html';
const rules = {}; const rules = {};
@@ -431,7 +432,7 @@ class ContestCreateHandler extends ContestHandler {
throw new ValidationError('beginAtDate', 'beginAtTime'); throw new ValidationError('beginAtDate', 'beginAtTime');
} }
const endAt = beginAt.clone().add(duration, 'hours'); const endAt = beginAt.clone().add(duration, 'hours');
pids = await this.verifyProblems(domainId, pids); pids = await contest.verifyProblems(domainId, pids);
const tid = await contest.add( const tid = await contest.add(
domainId, title, content, domainId, title, content,
this.user._id, rule, beginAt.toDate(), endAt.toDate(), pids, rated, this.user._id, rule, beginAt.toDate(), endAt.toDate(), pids, rated,
@@ -448,7 +449,6 @@ export async function apply() {
Route('contest_boardcast', '/contest/:tid/boardcast', ContestBoardcastHandler); Route('contest_boardcast', '/contest/:tid/boardcast', ContestBoardcastHandler);
Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST); Route('contest_edit', '/contest/:tid/edit', ContestEditHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST); Route('contest_scoreboard', '/contest/:tid/scoreboard', ContestScoreboardHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_scoreboard_raw', '/contest/:tid/scoreboard/raw', ContestScoreboardRawHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_scoreboard_download', '/contest/:tid/export/:ext', ContestScoreboardDownloadHandler, PERM.PERM_VIEW_CONTEST); Route('contest_scoreboard_download', '/contest/:tid/export/:ext', ContestScoreboardDownloadHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_detail_problem', '/contest/:tid/p/:pid', ContestProblemHandler, PERM.PERM_VIEW_CONTEST); Route('contest_detail_problem', '/contest/:tid/p/:pid', ContestProblemHandler, PERM.PERM_VIEW_CONTEST);
Route('contest_detail_problem_file_download', '/contest/:tid/p/:pid/file/:filename', ContestProblemFileDownloadHandler, PERM.PERM_VIEW_PROBLEM); Route('contest_detail_problem_file_download', '/contest/:tid/p/:pid/file/:filename', ContestProblemFileDownloadHandler, PERM.PERM_VIEW_PROBLEM);
+24 -21
View File
@@ -7,7 +7,7 @@ import {
ValidationError, HomeworkNotLiveError, ProblemNotFoundError, ValidationError, HomeworkNotLiveError, ProblemNotFoundError,
HomeworkNotAttendedError, HomeworkNotAttendedError,
} from '../error'; } from '../error';
import { PenaltyRules } from '../interface'; import { PenaltyRules, Tdoc, Pdoc } from '../interface';
import { import {
Route, Handler, Types, param, Route, Handler, Types, param,
} from '../service/server'; } from '../service/server';
@@ -29,9 +29,7 @@ const validatePenaltyRules = (input: string) => {
}; };
const convertPenaltyRules = validatePenaltyRules; const convertPenaltyRules = validatePenaltyRules;
const HomeworkHandler = contest.ContestHandlerMixin(Handler); class HomeworkMainHandler extends Handler {
class HomeworkMainHandler extends HomeworkHandler {
async get({ domainId }) { async get({ domainId }) {
const tdocs = await contest.getMulti(domainId, {}, document.TYPE_HOMEWORK).toArray(); const tdocs = await contest.getMulti(domainId, {}, document.TYPE_HOMEWORK).toArray();
const calendar = []; const calendar = [];
@@ -54,7 +52,7 @@ class HomeworkMainHandler extends HomeworkHandler {
} }
} }
class HomeworkDetailHandler extends HomeworkHandler { class HomeworkDetailHandler extends Handler {
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@param('page', Types.PositiveInt, true) @param('page', Types.PositiveInt, true)
async get(domainId: string, tid: ObjectID, page = 1) { async get(domainId: string, tid: ObjectID, page = 1) {
@@ -72,7 +70,7 @@ class HomeworkDetailHandler extends HomeworkHandler {
psdict[pdetail.pid] = pdetail; psdict[pdetail.pid] = pdetail;
rdict[pdetail.rid] = { _id: pdetail.rid }; rdict[pdetail.rid] = { _id: pdetail.rid };
} }
if (this.canShowRecord(tdoc) && tsdoc.journal) { if (contest.canShowRecord.call(this, tdoc) && tsdoc.journal) {
rdict = await record.getList( rdict = await record.getList(
domainId, domainId,
tsdoc.journal.map((pdetail) => pdetail.rid), tsdoc.journal.map((pdetail) => pdetail.rid),
@@ -118,7 +116,11 @@ class HomeworkDetailHandler extends HomeworkHandler {
} }
} }
class HomeworkDetailProblemHandler extends HomeworkHandler { class HomeworkDetailProblemHandler extends Handler {
tdoc: Tdoc<60>;
pdoc: Pdoc;
tsdoc: any;
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@param('pid', Types.UnsignedInt) @param('pid', Types.UnsignedInt)
async prepare(domainId: string, tid: ObjectID, pid: number) { async prepare(domainId: string, tid: ObjectID, pid: number) {
@@ -162,7 +164,7 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
if (!attended) throw new HomeworkNotAttendedError(tid); if (!attended) throw new HomeworkNotAttendedError(tid);
if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid); if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid);
if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid, tid); if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid, tid);
const rdocs = this.canShowRecord(this.tdoc) const rdocs = contest.canShowRecord.call(this, this.tdoc)
? await record.getUserInProblemMulti(domainId, this.user._id, pid, true) ? await record.getUserInProblemMulti(domainId, this.user._id, pid, true)
.sort('_id', -1).limit(10).toArray() .sort('_id', -1).limit(10).toArray()
: []; : [];
@@ -202,12 +204,12 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
]); ]);
bus.boardcast('record/change', rdoc); bus.boardcast('record/change', rdoc);
this.response.body = { tid, rid }; this.response.body = { tid, rid };
if (this.canShowRecord(this.tdoc)) this.response.redirect = this.url('record_detail', { rid }); if (contest.canShowRecord.call(this, this.tdoc)) this.response.redirect = this.url('record_detail', { rid });
else this.response.redirect = this.url('homework_detail', { tid }); else this.response.redirect = this.url('homework_detail', { tid });
} }
} }
class HomeworkCreateHandler extends HomeworkHandler { class HomeworkCreateHandler extends Handler {
async get() { async get() {
const beginAt = moment().add(1, 'day'); const beginAt = moment().add(1, 'day');
const penaltySince = beginAt.clone().add(7, 'days'); const penaltySince = beginAt.clone().add(7, 'days');
@@ -255,7 +257,7 @@ class HomeworkCreateHandler extends HomeworkHandler {
const endAt = penaltySince.clone().add(extensionDays, 'days'); const endAt = penaltySince.clone().add(extensionDays, 'days');
if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime'); if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime');
if (penaltySince.isAfter(endAt)) throw new ValidationError('extensionDays'); if (penaltySince.isAfter(endAt)) throw new ValidationError('extensionDays');
const verified = await this.verifyProblems(domainId, pids); const verified = await contest.verifyProblems(domainId, pids);
const tid = await contest.add(domainId, title, content, this.user._id, const tid = await contest.add(domainId, title, content, this.user._id,
'homework', beginAt.toDate(), endAt.toDate(), verified, rated, 'homework', beginAt.toDate(), endAt.toDate(), verified, rated,
{ penaltySince: penaltySince.toDate(), penaltyRules }, document.TYPE_HOMEWORK); { penaltySince: penaltySince.toDate(), penaltyRules }, document.TYPE_HOMEWORK);
@@ -264,7 +266,7 @@ class HomeworkCreateHandler extends HomeworkHandler {
} }
} }
class HomeworkEditHandler extends HomeworkHandler { class HomeworkEditHandler extends Handler {
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) { async get(domainId: string, tid: ObjectID) {
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
@@ -316,7 +318,7 @@ class HomeworkEditHandler extends HomeworkHandler {
return i; return i;
}); });
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_HOMEWORK); if (!this.user.own(tdoc)) this.checkPerm(PERM.PERM_EDIT_HOMEWORK);
else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF); else this.checkPerm(PERM.PERM_EDIT_HOMEWORK_SELF);
let beginAt; let beginAt;
let penaltySince; let penaltySince;
@@ -332,7 +334,7 @@ class HomeworkEditHandler extends HomeworkHandler {
} }
let endAt = penaltySince.clone().add(extensionDays, 'days'); let endAt = penaltySince.clone().add(extensionDays, 'days');
if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime'); if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime');
const verified = await this.verifyProblems(domainId, pids); const verified = await contest.verifyProblems(domainId, pids);
beginAt = beginAt.toDate(); beginAt = beginAt.toDate();
endAt = endAt.toDate(); endAt = endAt.toDate();
penaltySince = penaltySince.toDate(); penaltySince = penaltySince.toDate();
@@ -350,11 +352,12 @@ class HomeworkEditHandler extends HomeworkHandler {
} }
} }
class HomeworkScoreboardHandler extends HomeworkHandler { class HomeworkScoreboardHandler extends Handler {
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) { @param('page', Types.PositiveInt, true)
const [tdoc, rows, udict] = await this.getScoreboard( async get(domainId: string, tid: ObjectID, page = 1) {
domainId, tid, false, document.TYPE_HOMEWORK, const [tdoc, rows, udict] = await contest.getScoreboard(
domainId, tid, false, page, document.TYPE_HOMEWORK,
); );
const path = [ const path = [
['Hydro', 'homepage'], ['Hydro', 'homepage'],
@@ -369,7 +372,7 @@ class HomeworkScoreboardHandler extends HomeworkHandler {
} }
} }
class HomeworkScoreboardDownloadHandler extends HomeworkHandler { class HomeworkScoreboardDownloadHandler extends Handler {
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
@param('ext', Types.Name) @param('ext', Types.Name)
async get(domainId: string, tid: ObjectID, ext: string) { async get(domainId: string, tid: ObjectID, ext: string) {
@@ -378,12 +381,12 @@ class HomeworkScoreboardDownloadHandler extends HomeworkHandler {
html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }), html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }),
}; };
if (!getContent[ext]) throw new ValidationError('ext'); if (!getContent[ext]) throw new ValidationError('ext');
const [tdoc, rows] = await this.getScoreboard(domainId, tid, true, document.TYPE_HOMEWORK); const [tdoc, rows] = await contest.getScoreboard(domainId, tid, true, 0, document.TYPE_HOMEWORK);
this.binary(await getContent[ext](rows), `${tdoc.title}.${ext}`); this.binary(await getContent[ext](rows), `${tdoc.title}.${ext}`);
} }
} }
class HomeworkCodeHandler extends HomeworkHandler { class HomeworkCodeHandler extends Handler {
@param('tid', Types.ObjectID) @param('tid', Types.ObjectID)
async get(domainId: string, tid: ObjectID) { async get(domainId: string, tid: ObjectID) {
this.checkPerm(PERM.PERM_READ_RECORD_CODE); this.checkPerm(PERM.PERM_READ_RECORD_CODE);
+11 -13
View File
@@ -14,9 +14,7 @@ import {
Route, Handler, Connection, ConnectionHandler, Types, param, Route, Handler, Connection, ConnectionHandler, Types, param,
} from '../service/server'; } from '../service/server';
const RecordHandler = contest.ContestHandlerMixin(Handler); class RecordListHandler extends Handler {
class RecordListHandler extends RecordHandler {
@param('page', Types.PositiveInt, true) @param('page', Types.PositiveInt, true)
@param('pid', Types.Name, true) @param('pid', Types.Name, true)
@param('tid', Types.ObjectID, true) @param('tid', Types.ObjectID, true)
@@ -28,7 +26,7 @@ class RecordListHandler extends RecordHandler {
if (tid) { if (tid) {
const tdoc = await contest.get(domainId, tid, -1); const tdoc = await contest.get(domainId, tid, -1);
if (!tdoc) throw new ContestNotFoundError(domainId, pid); if (!tdoc) throw new ContestNotFoundError(domainId, pid);
if (!this.canShowScoreboard(tdoc, true)) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); if (!contest.canShowScoreboard.call(this, tdoc, true)) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
} }
if (uidOrName) { if (uidOrName) {
let udoc = await user.getById(domainId, +uidOrName); let udoc = await user.getById(domainId, +uidOrName);
@@ -55,7 +53,7 @@ class RecordListHandler extends RecordHandler {
user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)), user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)),
canViewProblem canViewProblem
? problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), canViewProblemHidden, false) ? problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), canViewProblemHidden, false)
: Object.fromEntries([rdocs.map((rdoc) => [rdoc.pid, problem.create(rdoc.pid, rdoc.pid)])]), : Object.fromEntries([rdocs.map((rdoc) => [rdoc.pid, problem.create(rdoc.pid, rdoc.pid.toString())])]),
]); ]);
const path = [ const path = [
['Hydro', 'homepage'], ['Hydro', 'homepage'],
@@ -80,7 +78,7 @@ class RecordListHandler extends RecordHandler {
} }
} }
class RecordDetailHandler extends RecordHandler { class RecordDetailHandler extends Handler {
@param('rid', Types.ObjectID) @param('rid', Types.ObjectID)
async get(domainId: string, rid: ObjectID) { async get(domainId: string, rid: ObjectID) {
this.response.template = 'record_detail.html'; this.response.template = 'record_detail.html';
@@ -88,7 +86,7 @@ class RecordDetailHandler extends RecordHandler {
if (!rdoc) throw new RecordNotFoundError(rid); if (!rdoc) throw new RecordNotFoundError(rid);
if (rdoc.contest) { if (rdoc.contest) {
const tdoc = await contest.get(domainId, rdoc.contest.tid, rdoc.contest.type); const tdoc = await contest.get(domainId, rdoc.contest.tid, rdoc.contest.type);
if (!this.canShowRecord(tdoc, true)) throw new PermissionError(rid); if (!contest.canShowRecord.call(this, tdoc, true)) throw new PermissionError(rid);
} }
if (rdoc.uid !== this.user._id && !this.user.hasPerm(PERM.PERM_READ_RECORD_CODE)) rdoc.code = null; if (rdoc.uid !== this.user._id && !this.user.hasPerm(PERM.PERM_READ_RECORD_CODE)) rdoc.code = null;
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
@@ -148,16 +146,15 @@ class RecordDetailHandler extends RecordHandler {
} }
} }
const RecordConnectionHandler = contest.ContestHandlerMixin(ConnectionHandler); class RecordMainConnectionHandler extends ConnectionHandler {
class RecordMainConnectionHandler extends RecordConnectionHandler {
dispose: bus.Disposable; dispose: bus.Disposable;
tid: string;
@param('tid', Types.ObjectID, true) @param('tid', Types.ObjectID, true)
async prepare(domainId: string, tid?: ObjectID) { async prepare(domainId: string, tid?: ObjectID) {
if (tid) { if (tid) {
const tdoc = await contest.get(domainId, tid, -1); const tdoc = await contest.get(domainId, tid, -1);
if (this.canShowRecord(tdoc)) this.tid = tid.toHexString(); if (contest.canShowRecord.call(this, tdoc)) this.tid = tid.toHexString();
else throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); else throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
} }
this.dispose = bus.on('record/change', this.onRecordChange.bind(this)); this.dispose = bus.on('record/change', this.onRecordChange.bind(this));
@@ -192,15 +189,16 @@ class RecordMainConnectionHandler extends RecordConnectionHandler {
} }
} }
class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(ConnectionHandler) { class RecordDetailConnectionHandler extends ConnectionHandler {
dispose: bus.Disposable; dispose: bus.Disposable;
rid: string;
@param('rid', Types.ObjectID) @param('rid', Types.ObjectID)
async prepare(domainId: string, rid: ObjectID) { async prepare(domainId: string, rid: ObjectID) {
const rdoc = await record.get(domainId, rid); const rdoc = await record.get(domainId, rid);
if (rdoc.contest) { if (rdoc.contest) {
const tdoc = await contest.get(domainId, rdoc.contest.tid, -1); const tdoc = await contest.get(domainId, rdoc.contest.tid, -1);
if (!this.canShowRecord(tdoc)) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); if (!contest.canShowRecord.call(this, tdoc)) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
} }
this.rid = rid.toString(); this.rid = rid.toString();
this.dispose = bus.on('record/change', this.onRecordChange.bind(this)); this.dispose = bus.on('record/change', this.onRecordChange.bind(this));
+1 -1
View File
@@ -84,7 +84,7 @@ class TrainingMainHandler extends Handler {
tdict = await training.getList(domainId, Array.from(enrolledTids)); tdict = await training.getList(domainId, Array.from(enrolledTids));
} }
} }
for (const tdoc of tdocs) tdict[tdoc.docId] = tdoc; for (const tdoc of tdocs) tdict[tdoc.docId.toHexString()] = tdoc;
const path = [ const path = [
['Hydro', 'homepage'], ['Hydro', 'homepage'],
['training_main', null], ['training_main', null],
+10 -10
View File
@@ -1,5 +1,5 @@
import type { Readable, Writable } from 'stream'; import type { Readable, Writable } from 'stream';
import type { ObjectID } from 'mongodb'; import type { Cursor, ObjectID } from 'mongodb';
import type fs from 'fs'; import type fs from 'fs';
import type { Dictionary, NumericDictionary } from 'lodash'; import type { Dictionary, NumericDictionary } from 'lodash';
import type * as Koa from 'koa'; import type * as Koa from 'koa';
@@ -400,17 +400,17 @@ export interface ContestStat extends Record<string, any> {
} }
export interface ContestRule { export interface ContestRule {
TEXT: string, TEXT: string;
check: (args: any) => any, check: (args: any) => any;
statusSort: any, statusSort: any;
showScoreboard: (tdoc: Tdoc<30 | 60>, now: Date) => boolean, showScoreboard: (tdoc: Tdoc<30 | 60>, now: Date) => boolean;
showRecord: (tdoc: Tdoc<30 | 60>, now: Date) => boolean, showRecord: (tdoc: Tdoc<30 | 60>, now: Date) => boolean;
stat: (tdoc: Tdoc<30 | 60>, journal: any[]) => ContestStat, stat: (tdoc: Tdoc<30 | 60>, journal: any[]) => ContestStat;
scoreboard: ( scoreboard: (
isExport: boolean, _: (s: string) => string, isExport: boolean, _: (s: string) => string,
tdoc: Tdoc<30 | 60>, rankedTsdocs: any[], udict: Udict, pdict: Pdict tdoc: Tdoc<30 | 60>, pdict: Pdict, cursor: Cursor<any>, page: number,
) => ScoreboardRow[], ) => Promise<[board: ScoreboardRow[], udict: Udict]>;
rank: (tsdocs: any[]) => any[], ranked: (tdoc: Tdoc<30 | 60>, cursor: Cursor<any>) => Promise<any[]>;
} }
export type ContestRules = Dictionary<ContestRule>; export type ContestRules = Dictionary<ContestRule>;
+3 -3
View File
@@ -1,9 +1,9 @@
import { Cursor } from 'mongodb'; import { Cursor } from 'mongodb';
import { ValidationError } from '../error'; import { ValidationError } from '../error';
async function paginate( async function paginate<T>(
cursor: Cursor, page: number, pageSize: number, cursor: Cursor<T>, page: number, pageSize: number,
): Promise<[any[]/* docs */, number/* numPages */, number/* count */]> { ): Promise<[docs: T[], numPages: number, count: number]> {
if (page <= 0) throw new ValidationError('page'); if (page <= 0) throw new ValidationError('page');
const [count, pageDocs] = await Promise.all([ const [count, pageDocs] = await Promise.all([
cursor.count(), cursor.count(),
+45 -6
View File
@@ -1,16 +1,55 @@
function ranked<T>(diter: T[], equ = (a: T, b: T) => a === b): [number, T][] { import { Cursor } from 'mongodb';
import paginate from './paginate';
type G<T> = Promise<[[number, T], () => G<T>] | []>;
interface ranked {
<T>(cursor: Cursor<T>, equ: (a: T, b: T) => boolean, page: number): Promise<[number, T][]>;
iterate<T>(cursor: Cursor<T>, equ: (a: T, b: T) => boolean): G<T>;
all<T>(cursor: Cursor<T>, equ: (a: T, b: T) => boolean): Promise<[number, T][]>;
}
const ranked: ranked = async function rankedPage(cursor, equ = (a, b) => a === b, page) {
let last = null; let last = null;
let r = 0; let r = (page - 1) * 100;
let count = 0; let count = r;
const results: [number, T][] = []; const results = [];
for (const doc of diter) { const [docs] = await paginate(cursor, page, 100);
for (const doc of docs) {
count++; count++;
if (count === 1 || !equ(last, doc)) r = count; if (count === 1 || !equ(last, doc)) r = count;
last = doc; last = doc;
results.push([r, doc]); results.push([r, doc]);
} }
return results; return results;
} };
ranked.iterate = async function rankedIterate<T>(cursor: Cursor<T>, equ = (a, b) => a === b) {
let last = null;
let r = 0;
let count = 0;
async function getNext(): G<T> {
if (!await cursor.hasNext()) return [];
const doc = await cursor.next();
count++;
if (count === 1 || !equ(last, doc)) r = count;
last = doc;
return [[r, doc], getNext];
}
return await getNext();
};
ranked.all = async function rankedAll(cursor, equ = (a, b) => a === b) {
let last = null;
let r = 0;
let count = 0;
const results = [];
const docs = await cursor.toArray();
for (const doc of docs) {
count++;
if (count === 1 || !equ(last, doc)) r = count;
last = doc;
results.push([r, doc]);
}
return results;
};
global.Hydro.lib.rank = ranked; global.Hydro.lib.rank = ranked;
export = ranked; export = ranked;
+72 -68
View File
@@ -8,7 +8,9 @@ import {
ContestNotAttendedError, ContestScoreboardHiddenError, ContestNotAttendedError, ContestScoreboardHiddenError,
} from '../error'; } from '../error';
import { import {
ContestRule, ContestRules, ScoreboardNode, ScoreboardRow, Tdoc, Udict, ContestRule, ContestRules, Pdict,
ScoreboardNode, ScoreboardRow, Tdoc,
Udict,
} from '../interface'; } from '../interface';
import * as misc from '../lib/misc'; import * as misc from '../lib/misc';
import ranked from '../lib/rank'; import ranked from '../lib/rank';
@@ -50,7 +52,10 @@ const acm: ContestRule = {
} }
return { accept, time, detail }; return { accept, time, detail };
}, },
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { async scoreboard(isExport, _, tdoc, pdict, cursor, page) {
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score, page);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids);
const columns: ScoreboardRow = [ const columns: ScoreboardRow = [
{ type: 'rank', value: _('Rank') }, { type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') }, { type: 'user', value: _('User') },
@@ -93,7 +98,7 @@ const acm: ContestRule = {
for (const item of tsdoc.detail || []) tsddict[item.pid] = item; for (const item of tsdoc.detail || []) tsddict[item.pid] = item;
const row: ScoreboardRow = []; const row: ScoreboardRow = [];
row.push( row.push(
{ type: 'string', value: rank }, { type: 'string', value: rank.toString() },
{ type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid }, { type: 'user', value: udict[tsdoc.uid].uname, raw: tsdoc.uid },
{ type: 'string', value: tsdoc.accept || 0 }, { type: 'string', value: tsdoc.accept || 0 },
); );
@@ -134,9 +139,11 @@ const acm: ContestRule = {
row.raw = tsdoc; row.raw = tsdoc;
rows.push(row); rows.push(row);
} }
return rows; return [rows, udict];
},
async ranked(tdoc, cursor) {
return await ranked.all(cursor, (a, b) => a.score === b.score);
}, },
rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score),
}; };
const oi: ContestRule = { const oi: ContestRule = {
@@ -154,7 +161,10 @@ const oi: ContestRule = {
}, },
showScoreboard: (tdoc, now) => now > tdoc.endAt, showScoreboard: (tdoc, now) => now > tdoc.endAt,
showRecord: (tdoc, now) => now > tdoc.endAt, showRecord: (tdoc, now) => now > tdoc.endAt,
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { async scoreboard(isExport, _, tdoc, pdict, cursor, page) {
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score, page);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList(tdoc.domainId, uids);
const columns: ScoreboardNode[] = [ const columns: ScoreboardNode[] = [
{ type: 'rank', value: _('Rank') }, { type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') }, { type: 'user', value: _('User') },
@@ -195,9 +205,11 @@ const oi: ContestRule = {
} }
rows.push(row); rows.push(row);
} }
return rows; return [rows, udict];
},
async ranked(tdoc, cursor) {
return await ranked.all(cursor, (a, b) => a.score === b.score);
}, },
rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score),
}; };
const ioi: ContestRule = { const ioi: ContestRule = {
@@ -255,7 +267,10 @@ const homework: ContestRule = {
}, },
showScoreboard: () => true, showScoreboard: () => true,
showRecord: () => true, showRecord: () => true,
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { async scoreboard(isExport, _, tdoc, pdict, cursor, page) {
const rankedTsdocs = await ranked(cursor, (a, b) => a.score === b.score, page);
const uids = rankedTsdocs.map(([, tsdoc]) => tsdoc.uid);
const udict = await user.getList.call(this, tdoc.domainId, uids);
const columns: ScoreboardNode[] = [ const columns: ScoreboardNode[] = [
{ type: 'rank', value: _('Rank') }, { type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') }, { type: 'user', value: _('User') },
@@ -343,9 +358,11 @@ const homework: ContestRule = {
} }
rows.push(row); rows.push(row);
} }
return rows; return [rows, udict];
},
async ranked(tdoc, cursor) {
return await ranked.all(cursor, (a, b) => a.score === b.score);
}, },
rank: (tsdocs) => ranked(tsdocs, (a, b) => a.score === b.score),
}; };
export const RULES: ContestRules = { export const RULES: ContestRules = {
@@ -483,62 +500,6 @@ export function isExtended(tdoc: Tdoc) {
return tdoc.penaltySince.getTime() <= now && now < tdoc.endAt.getTime(); return tdoc.penaltySince.getTime() <= now && now < tdoc.endAt.getTime();
} }
export const ContestHandlerMixin = (c) => class extends c {
canViewHiddenScoreboard() {
return this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
}
canShowRecord(tdoc: Tdoc<30 | 60>, allowPermOverride = true) {
if (RULES[tdoc.rule].showRecord(tdoc, new Date())) return true;
if (allowPermOverride && this.canViewHiddenScoreboard()) return true;
return false;
}
canShowScoreboard(tdoc: Tdoc<30 | 60>, allowPermOverride = true) {
if (RULES[tdoc.rule].showScoreboard(tdoc, new Date())) return true;
if (allowPermOverride && this.canViewHiddenScoreboard()) return true;
return false;
}
async getScoreboard(
domainId: string, tid: ObjectID,
isExport = false, docType: 30 | 60 = document.TYPE_CONTEST,
): Promise<[Tdoc<30 | 60>, ScoreboardRow[], Udict]> {
const {
tdoc, udict, pdict, rankedTsdocs,
} = await this.getRawStatus(domainId, tid, docType);
const rows = RULES[tdoc.rule].scoreboard(
isExport, this.translate.bind(this),
tdoc, rankedTsdocs, udict, pdict,
);
return [tdoc, rows, udict];
}
async getRawStatus(domainId: string, tid: ObjectID, docType: 30 | 60 = document.TYPE_CONTEST) {
const tdoc = await get(domainId, tid, docType);
if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid);
if (!this.canShowRecord(tdoc)) throw new ContestScoreboardHiddenError(tid);
const tsdocs = await getMultiStatus(domainId, { docId: tid }, docType).sort(RULES[tdoc.rule].statusSort).toArray();
const uids = tsdocs.map((tsdoc) => tsdoc.uid);
const [udict, pdict] = await Promise.all([
user.getList(domainId, uids),
problem.getList(domainId, tdoc.pids, true),
]);
const rankedTsdocs = RULES[tdoc.rule].rank(tsdocs);
return {
tdoc, tsdocs, udict, pdict, rankedTsdocs,
};
}
// eslint-disable-next-line class-methods-use-this
async verifyProblems(domainId: string, pids: Array<number | string>) {
const pdict = await problem.getList(domainId, pids, true);
const _pids: Set<number> = new Set();
for (const i in pdict) _pids.add(pdict[i].docId);
return Array.from(_pids);
}
};
export function setStatus(domainId: string, tid: ObjectID, uid: number, $set: any) { export function setStatus(domainId: string, tid: ObjectID, uid: number, $set: any) {
return document.setStatus(domainId, document.TYPE_CONTEST, tid, uid, $set); return document.setStatus(domainId, document.TYPE_CONTEST, tid, uid, $set);
} }
@@ -584,6 +545,45 @@ export async function recalcStatus(domainId: string, tid: ObjectID, type: Type =
return await Promise.all(tasks); return await Promise.all(tasks);
} }
export function canViewHiddenScoreboard() {
return this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
}
export function canShowRecord(tdoc: Tdoc<30 | 60>, allowPermOverride = true) {
if (RULES[tdoc.rule].showRecord(tdoc, new Date())) return true;
if (allowPermOverride && this.canViewHiddenScoreboard()) return true;
return false;
}
export function canShowScoreboard(tdoc: Tdoc<30 | 60>, allowPermOverride = true) {
if (RULES[tdoc.rule].showScoreboard(tdoc, new Date())) return true;
if (allowPermOverride && this.canViewHiddenScoreboard()) return true;
return false;
}
export async function getScoreboard(
domainId: string, tid: ObjectID,
isExport = false, page: number, docType: 30 | 60 = document.TYPE_CONTEST,
): Promise<[Tdoc<30 | 60>, ScoreboardRow[], Udict, Pdict]> {
const tdoc = await get(domainId, tid, docType);
if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid);
if (!this.canShowRecord(tdoc)) throw new ContestScoreboardHiddenError(tid);
const tsdocs = await getMultiStatus(domainId, { docId: tid }, docType).sort(RULES[tdoc.rule].statusSort);
const pdict = await problem.getList(domainId, tdoc.pids, true);
const [rows, udict] = await RULES[tdoc.rule].scoreboard(
isExport, this.translate.bind(this),
tdoc, pdict, tsdocs, page,
);
return [tdoc, rows, udict, pdict];
}
export async function verifyProblems(domainId: string, pids: Array<number | string>) {
const pdict = await problem.getList(domainId, pids, true);
const _pids: Set<number> = new Set();
for (const i in pdict) _pids.add(pdict[i].docId);
return Array.from(_pids);
}
export const statusText = (tdoc: Tdoc) => ( export const statusText = (tdoc: Tdoc) => (
isNew(tdoc) isNew(tdoc)
? 'New' ? 'New'
@@ -602,7 +602,6 @@ export const getStatusText = (tdoc: Tdoc) => (
global.Hydro.model.contest = { global.Hydro.model.contest = {
RULES, RULES,
ContestHandlerMixin,
add, add,
getListStatus, getListStatus,
getMultiStatus, getMultiStatus,
@@ -617,6 +616,11 @@ global.Hydro.model.contest = {
setStatus, setStatus,
getAndListStatus, getAndListStatus,
recalcStatus, recalcStatus,
canShowRecord,
canShowScoreboard,
canViewHiddenScoreboard,
getScoreboard,
verifyProblems,
isNew, isNew,
isUpcoming, isUpcoming,
isNotStarted, isNotStarted,
+5 -4
View File
@@ -94,17 +94,18 @@ export async function add(
} }
// FIXME typings doesn't work // FIXME typings doesn't work
export function get<T extends Ddoc.Field>(domainId: string, did: ObjectID, projection: T[] = PROJECTION_PUBLIC as any): Promise<Pick<Ddoc, T>> { export async function get<T extends Ddoc.Field>(domainId: string, did: ObjectID, projection: T[] = PROJECTION_PUBLIC as any): Promise<Pick<Ddoc, T>> {
return document.get(domainId, document.TYPE_DISCUSSION, did, projection); return await document.get(domainId, document.TYPE_DISCUSSION, did, projection);
} }
export function edit( export function edit(
domainId: string, did: ObjectID, domainId: string, did: ObjectID,
title: string, content: string, highlight: boolean, pin: boolean, title: string, content: string, highlight: boolean, pin: boolean,
): Promise<Ddoc | null> { ): Promise<Ddoc | null> {
return document.set(domainId, document.TYPE_DISCUSSION, did, { const payload = {
title, content, highlight, pin, title, content, highlight, pin,
}); };
return document.set(domainId, document.TYPE_DISCUSSION, did, payload);
} }
export function del(domainId: string, did: ObjectID): Promise<never> { export function del(domainId: string, did: ObjectID): Promise<never> {
+37 -6
View File
@@ -1,4 +1,5 @@
import { Collection } from 'mongodb'; import { Collection } from 'mongodb';
import LRU from 'lru-cache';
import { escapeRegExp } from 'lodash'; import { escapeRegExp } from 'lodash';
import * as system from './system'; import * as system from './system';
import token from './token'; import token from './token';
@@ -16,6 +17,16 @@ import * as bus from '../service/bus';
const coll: Collection<Udoc> = db.collection('user'); const coll: Collection<Udoc> = db.collection('user');
const logger = new Logger('model/user'); const logger = new Logger('model/user');
const cache = new LRU<string, User>(500);
function deleteUserCache(udoc: User | Udoc) {
if (udoc) {
const id = [udoc._id.toString(), udoc.uname.toLowerCase(), udoc.mail.toLowerCase()];
for (const key of cache.keys().filter((k) => id.includes(k.split('/')[0]))) {
cache.del(key);
}
}
}
class User implements _User { class User implements _User {
_id: number; _id: number;
@@ -125,13 +136,18 @@ class UserModel {
@ArgMethod @ArgMethod
static async getById(domainId: string, _id: number, scope: bigint | string = PERM.PERM_ALL): Promise<User | null> { static async getById(domainId: string, _id: number, scope: bigint | string = PERM.PERM_ALL): Promise<User | null> {
if (cache.has(`${_id}/${domainId}`)) return cache.get(`${_id}/${domainId}`);
const udoc = _id === 0 || _id === 1 const udoc = _id === 0 || _id === 1
? BUILTIN_USERS[_id] ? BUILTIN_USERS[_id]
: await coll.findOne({ _id }); : await coll.findOne({ _id });
if (!udoc) return null; if (!udoc) return null;
const dudoc = await domain.getDomainUser(domainId, udoc); const dudoc = await domain.getDomainUser(domainId, udoc);
if (typeof scope === 'string') scope = BigInt(scope); if (typeof scope === 'string') scope = BigInt(scope);
return new UserModel.User(udoc, dudoc, scope).init(); const res = await new User(udoc, dudoc, scope).init();
cache.set(`${res._id}/${domainId}`, res);
cache.set(`${res.uname.toLowerCase()}/${domainId}`, res);
cache.set(`${res.mail.toLowerCase()}/${domainId}`, res);
return res;
} }
static async getList(domainId: string, uids: number[]): Promise<Udict> { static async getList(domainId: string, uids: number[]): Promise<Udict> {
@@ -145,6 +161,7 @@ class UserModel {
@ArgMethod @ArgMethod
static async getByUname(domainId: string, uname: string): Promise<User | null> { static async getByUname(domainId: string, uname: string): Promise<User | null> {
const unameLower = uname.trim().toLowerCase(); const unameLower = uname.trim().toLowerCase();
if (cache.has(`${unameLower}/${domainId}`)) return cache.get(`${unameLower}/${domainId}`);
const udoc = (unameLower === 'guest') const udoc = (unameLower === 'guest')
? BUILTIN_USERS[0] ? BUILTIN_USERS[0]
: unameLower === 'hydro' : unameLower === 'hydro'
@@ -152,12 +169,17 @@ class UserModel {
: await coll.findOne({ unameLower }); : await coll.findOne({ unameLower });
if (!udoc) return null; if (!udoc) return null;
const dudoc = await domain.getDomainUser(domainId, udoc); const dudoc = await domain.getDomainUser(domainId, udoc);
return new UserModel.User(udoc, dudoc).init(); const res = await new UserModel.User(udoc, dudoc).init();
cache.set(`${res._id}/${domainId}`, res);
cache.set(`${res.uname.toLowerCase()}/${domainId}`, res);
cache.set(`${res.mail.toLowerCase()}/${domainId}`, res);
return res;
} }
@ArgMethod @ArgMethod
static async getByEmail(domainId: string, mail: string): Promise<User | null> { static async getByEmail(domainId: string, mail: string): Promise<User | null> {
const mailLower = mail.trim().toLowerCase(); const mailLower = mail.trim().toLowerCase();
if (cache.has(`${mailLower}/${domainId}`)) return cache.get(`${mailLower}/${domainId}`);
const udoc = (mailLower === 'guest@hydro.local') const udoc = (mailLower === 'guest@hydro.local')
? BUILTIN_USERS[0] ? BUILTIN_USERS[0]
: mailLower === 'hydro@hydro.local' : mailLower === 'hydro@hydro.local'
@@ -165,7 +187,11 @@ class UserModel {
: await coll.findOne({ mailLower }); : await coll.findOne({ mailLower });
if (!udoc) return null; if (!udoc) return null;
const dudoc = await domain.getDomainUser(domainId, udoc); const dudoc = await domain.getDomainUser(domainId, udoc);
return new UserModel.User(udoc, dudoc).init(); const res = await new UserModel.User(udoc, dudoc).init();
cache.set(`${res._id}/${domainId}`, res);
cache.set(`${res.uname.toLowerCase()}/${domainId}`, res);
cache.set(`${res.mail.toLowerCase()}/${domainId}`, res);
return res;
} }
@ArgMethod @ArgMethod
@@ -173,7 +199,9 @@ class UserModel {
const op: any = {}; const op: any = {};
if ($set && Object.keys($set).length) op.$set = $set; if ($set && Object.keys($set).length) op.$set = $set;
if ($unset && Object.keys($unset).length) op.$unset = $unset; if ($unset && Object.keys($unset).length) op.$unset = $unset;
return await coll.findOneAndUpdate({ _id: uid }, op); const res = await coll.findOneAndUpdate({ _id: uid }, op, { returnOriginal: false });
deleteUserCache(res.value);
return res;
} }
@ArgMethod @ArgMethod
@@ -189,6 +217,7 @@ class UserModel {
{ $set: { salt, hash: pwhash(password, salt), hashType: 'hydro' } }, { $set: { salt, hash: pwhash(password, salt), hashType: 'hydro' } },
{ returnOriginal: false }, { returnOriginal: false },
); );
deleteUserCache(res.value);
return res.value; return res.value;
} }
@@ -198,6 +227,7 @@ class UserModel {
if (!udoc) throw new UserNotFoundError(_id); if (!udoc) throw new UserNotFoundError(_id);
udoc[field] = udoc[field] + n || n; udoc[field] = udoc[field] + n || n;
await coll.updateOne({ _id }, { $set: { [field]: udoc[field] } }); await coll.updateOne({ _id }, { $set: { [field]: udoc[field] } });
deleteUserCache(udoc);
return udoc; return udoc;
} }
@@ -252,12 +282,13 @@ class UserModel {
@ArgMethod @ArgMethod
static async setPriv(uid: number, priv: number): Promise<Udoc | null> { static async setPriv(uid: number, priv: number): Promise<Udoc | null> {
const udoc = await coll.findOneAndUpdate( const res = await coll.findOneAndUpdate(
{ _id: uid }, { _id: uid },
{ $set: { priv } }, { $set: { priv } },
{ returnOriginal: false }, { returnOriginal: false },
); );
return udoc.value; deleteUserCache(res.value);
return res.value;
} }
@ArgMethod @ArgMethod
+6 -6
View File
@@ -42,8 +42,8 @@ async function runProblem(...arg: any[]) {
); );
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), true);
for (const psdoc of psdocs) { for (const psdoc of psdocs) {
if (rdict[psdoc.rid]) { if (rdict[psdoc.rid.toHexString()]) {
const rp = rdict[psdoc.rid].score * p; const rp = rdict[psdoc.rid.toHexString()].score * p;
udict[psdoc.uid] = (udict[psdoc.uid] || 1500) + rp; udict[psdoc.uid] = (udict[psdoc.uid] || 1500) + rp;
} }
} }
@@ -61,10 +61,10 @@ async function runContest(...arg: any[]) {
: arg[0]; : arg[0];
const udict: ND = (typeof arg[0] === 'string') ? arg[2] : arg[1]; const udict: ND = (typeof arg[0] === 'string') ? arg[2] : arg[1];
const report = (typeof arg[0] === 'string') ? arg[3] : arg[2]; const report = (typeof arg[0] === 'string') ? arg[3] : arg[2];
const tsdocs = await contest.getMultiStatus(tdoc.domainId, tdoc.docId, tdoc.docType) const cursor = await contest.getMultiStatus(tdoc.domainId, tdoc.docId, tdoc.docType)
.sort(contest.RULES[tdoc.rule].statusSort).toArray(); .sort(contest.RULES[tdoc.rule].statusSort);
if (!tsdocs.length) return; if (!await cursor.count()) return;
const rankedTsdocs = contest.RULES[tdoc.rule].rank(tsdocs); const rankedTsdocs = await contest.RULES[tdoc.rule].ranked(tdoc, cursor);
const users = []; const users = [];
for (const result of rankedTsdocs) { for (const result of rankedTsdocs) {
users.push({ uid: result[1].uid, rank: result[0], old: udict[result[1].uid] || 1500 }); users.push({ uid: result[1].uid, rank: result[0], old: udict[result[1].uid] || 1500 });