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