Hello airbnb

This commit is contained in:
masnn committed 2020-04-15 15:08:05 +08:00
1 parent f9468536de
commit 8c195661d7
78 files changed
+2032 -1471

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
module.exports = {
env: {
commonjs: true,
node: true,
},
extends: [
'airbnb-base',
],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
},
parserOptions: {
ecmaVersion: 2018,
},
rules: {
"indent": ["warn", 4],
"no-plusplus": "off",
"no-underscore-dangle": "off",
"no-console": "off",
"no-extend-native": "off",
"no-restricted-syntax": "off",
"max-classes-per-file": "off",
"radix": "off",
"guard-for-in": "off",
"no-param-reassign": "off",
"global-require": "off"
},
};
-36
View File
@@ -1,36 +0,0 @@
{
"env": {
"commonjs": true,
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"indent": [
"warn",
4
],
"linebreak-style": [
"warn",
"unix"
],
"quotes": [
"warn",
"single"
],
"semi": [
"warn",
"always"
],
"no-unused-vars": "warn",
"no-unreachable": "warn",
"require-atomic-updates": "off"
}
}
+15 -14
View File
@@ -1,28 +1,21 @@
process.stdin.setEncoding('utf8'); process.stdin.setEncoding('utf8');
process.stdin.on('data', async input => { process.stdin.on('data', async (input) => {
try { try {
let t = eval(input.toString().trim()); const t = eval(input.toString().trim()); // eslint-disable-line no-eval
if (t instanceof Promise) console.log(await t); if (t instanceof Promise) console.log(await t);
else console.log(t); else console.log(t);
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
} }
}); });
process.on('restart', async () => {
console.log('Signal detected, restarting...');
await global.Hydro.stop();
await global.Hydro.destory();
delete global.Hydro;
delete require.cache;
run();
});
const path = require('path'); const path = require('path');
const EventEmitter = require('events');
const i18n = require('./lib/i18n'); const i18n = require('./lib/i18n');
i18n(path.resolve(__dirname, '..', 'locales', 'zh_CN.yaml'), 'zh_CN'); i18n(path.resolve(__dirname, '..', 'locales', 'zh_CN.yaml'), 'zh_CN');
i18n(path.resolve(__dirname, '..', 'locales', 'zh_TW.yaml'), 'zh_TW'); i18n(path.resolve(__dirname, '..', 'locales', 'zh_TW.yaml'), 'zh_TW');
i18n(path.resolve(__dirname, '..', 'locales', 'en.yaml'), 'en'); i18n(path.resolve(__dirname, '..', 'locales', 'en.yaml'), 'en');
const EventEmitter = require('events');
global.bus = new EventEmitter(); global.bus = new EventEmitter();
async function run() { async function run() {
require('./utils'); require('./utils');
@@ -34,7 +27,7 @@ async function run() {
}); });
}); });
require('./service/gridfs'); require('./service/gridfs');
let server = require('./service/server'); const server = require('./service/server');
require('./handler/home'); require('./handler/home');
require('./handler/problem'); require('./handler/problem');
require('./handler/record'); require('./handler/record');
@@ -43,7 +36,15 @@ async function run() {
require('./handler/contest'); require('./handler/contest');
server.start(); server.start();
} }
run().catch(e => { process.on('restart', async () => {
console.log('Signal detected, restarting...');
await global.Hydro.stop();
await global.Hydro.destory();
delete global.Hydro;
delete require.cache;
run();
});
run().catch((e) => {
console.error(e); console.error(e);
process.exit(1); process.exit(1);
}); });
+29 -15
View File
@@ -2,7 +2,7 @@ class UserFacingError extends Error {
constructor(type) { constructor(type) {
super(type); super(type);
this.code = 500; this.code = 500;
//this.stack = ''; // this.stack = '';
this.params = []; this.params = [];
} }
} }
@@ -75,9 +75,9 @@ class VerifyPasswordError extends ForbiddenError {
} }
} }
class OpcountExceededError extends ForbiddenError { class OpcountExceededError extends ForbiddenError {
constructor(op, period_secs, max_operations) { constructor(op, periodSecs, maxOperations) {
super('OpcountExceededError'); super('OpcountExceededError');
this.params = [op, period_secs, max_operations]; this.params = [op, periodSecs, maxOperations];
} }
} }
@@ -155,15 +155,31 @@ class ProblemDataNotFoundError extends NotFoundError {
} }
} }
module.exports = { module.exports = {
BadRequestError, ForbiddenError, NotFoundError, BadRequestError,
LoginError, UserAlreadyExistError, InvalidTokenError, ForbiddenError,
UserNotFoundError, VerifyPasswordError, ProblemDataNotFoundError, NotFoundError,
OpcountExceededError, PermissionError, NoProblemError, LoginError,
ValidationError, ProblemNotFoundError, TrainingNotFoundError, UserAlreadyExistError,
ContestNotFoundError, RecordNotFoundError, SolutionNotFoundError, InvalidTokenError,
AlreadyVotedError, ContestNotAttendedError, ContestNotLiveError, UserNotFoundError,
ContestScoreboardHiddenError, ContestAlreadyAttendedError, UserFacingError, VerifyPasswordError,
SystemError ProblemDataNotFoundError,
OpcountExceededError,
PermissionError,
NoProblemError,
ValidationError,
ProblemNotFoundError,
TrainingNotFoundError,
ContestNotFoundError,
RecordNotFoundError,
SolutionNotFoundError,
AlreadyVotedError,
ContestNotAttendedError,
ContestNotLiveError,
ContestScoreboardHiddenError,
ContestAlreadyAttendedError,
UserFacingError,
SystemError,
}; };
/* /*
@@ -201,7 +217,6 @@ class UserFacingError(Error):
return 'An error has occurred.' return 'An error has occurred.'
class BlacklistedError(ForbiddenError): class BlacklistedError(ForbiddenError):
@property @property
def message(self): def message(self):
@@ -240,7 +255,6 @@ class InvalidOperationError(ForbiddenError):
pass pass
class InvalidTokenDigestError(ForbiddenError): class InvalidTokenDigestError(ForbiddenError):
pass pass
@@ -374,4 +388,4 @@ class SendMailError(UserFacingError):
@property @property
def message(self): def message(self):
return 'Failed to send mail to {0}.' return 'Failed to send mail to {0}.'
*/ */
+114 -78
View File
@@ -1,20 +1,23 @@
const const {
{ ContestNotLiveError, ValidationError, ProblemNotFoundError, ContestNotLiveError, ValidationError, ProblemNotFoundError,
ContestNotAttendedError } = require('../error'), ContestNotAttendedError,
{ PERM_CREATE_CONTEST, PERM_EDIT_CONTEST } = require('../permission'), } = require('../error');
{ constants } = require('../options'), const { PERM_CREATE_CONTEST, PERM_EDIT_CONTEST } = require('../permission');
paginate = require('../lib/paginate'), const { constants } = require('../options');
contest = require('../model/contest'), const paginate = require('../lib/paginate');
problem = require('../model/problem'), const contest = require('../model/contest');
record = require('../model/record'), const problem = require('../model/problem');
user = require('../model/user'), const record = require('../model/record');
{ Route, Handler } = require('../service/server'); const user = require('../model/user');
const { Route, Handler } = require('../service/server');
const ContestHandler = contest.ContestHandlerMixin(Handler); const ContestHandler = contest.ContestHandlerMixin(Handler);
class ContestListHandler extends ContestHandler { class ContestListHandler extends ContestHandler {
async get({ rule = 0, page = 1 }) { async get({ rule = 0, page = 1 }) {
this.response.template = 'contest_main.html'; this.response.template = 'contest_main.html';
let tdocs, qs, tpcount; let tdocs;
let qs;
let tpcount;
if (!rule) { if (!rule) {
tdocs = contest.getMulti().sort({ beginAt: -1 }); tdocs = contest.getMulti().sort({ beginAt: -1 });
qs = ''; qs = '';
@@ -23,12 +26,13 @@ class ContestListHandler extends ContestHandler {
tdocs = contest.getMulti({ rule }).sort({ beginAt: -1 }); tdocs = contest.getMulti({ rule }).sort({ beginAt: -1 });
qs = 'rule={0}'.format(rule); qs = 'rule={0}'.format(rule);
} }
// eslint-disable-next-line prefer-const
[tdocs, tpcount] = await paginate(tdocs, page, constants.CONTEST_PER_PAGE); [tdocs, tpcount] = await paginate(tdocs, page, constants.CONTEST_PER_PAGE);
let tids = []; const tids = [];
for (let tdoc of tdocs) tids.push(tdoc._id); for (const tdoc of tdocs) tids.push(tdoc._id);
let tsdict = await contest.getListStatus(this.user._id, tids); const tsdict = await contest.getListStatus(this.user._id, tids);
this.response.body = { this.response.body = {
page, tpcount, qs, rule, tdocs, tsdict page, tpcount, qs, rule, tdocs, tsdict,
}; };
} }
} }
@@ -36,35 +40,35 @@ class ContestDetailHandler extends ContestHandler {
async _prepare({ tid }) { async _prepare({ tid }) {
this.tdoc = await contest.get(tid); this.tdoc = await contest.get(tid);
} }
async get({ page = 1 }) { async get({ page = 1 }) {
this.response.template = 'contest_detail.html'; this.response.template = 'contest_detail.html';
let [tsdoc, pdict] = await Promise.all([ const [tsdoc, pdict] = await Promise.all([
contest.getStatus(this.tdoc._id, this.user._id), contest.getStatus(this.tdoc._id, this.user._id),
problem.getList(this.tdoc.pids) problem.getList(this.tdoc.pids),
]); ]);
let psdict = {}, rdict = {}, attended; const psdict = {}; let rdict = {}; let
attended;
if (tsdoc) { if (tsdoc) {
attended = tsdoc.attend == 1; attended = tsdoc.attend === 1;
for (let pdetail of tsdoc.journal || []) for (const pdetail of tsdoc.journal || []) { psdict[pdetail.pid] = pdetail; }
psdict[pdetail.pid] = pdetail;
if (this.canShowRecord(this.tdoc)) { if (this.canShowRecord(this.tdoc)) {
let q = []; const q = [];
for (let i in psdict) q.push(psdict[i].rid); for (const i in psdict) q.push(psdict[i].rid);
rdict = await record.getList(q); rdict = await record.getList(q);
} else } else { for (const i in psdict) rdict[psdict[i].rid] = { _id: psdict[i].rid }; }
for (let i in psdict)
rdict[psdict[i].rid] = { _id: psdict[i].rid };
} else attended = false; } else attended = false;
let udict = await user.getList([this.tdoc.owner]); const udict = await user.getList([this.tdoc.owner]);
let path = [ const path = [
['contest_main', '/c'], ['contest_main', '/c'],
[this.tdoc.title, null, true] [this.tdoc.title, null, true],
]; ];
this.response.body = { this.response.body = {
path, tdoc: this.tdoc, tsdoc, attended, udict, pdict, psdict, rdict, page path, tdoc: this.tdoc, tsdoc, attended, udict, pdict, psdict, rdict, page,
}; };
} }
async post_attend() {
async postAttend() {
if (contest.is_done(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id); if (contest.is_done(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id);
await contest.attend(this.tdoc._id, this.user._id); await contest.attend(this.tdoc._id, this.user._id);
this.back(); this.back();
@@ -72,108 +76,136 @@ class ContestDetailHandler extends ContestHandler {
} }
class ContestScoreboardHandler extends ContestDetailHandler { class ContestScoreboardHandler extends ContestDetailHandler {
async get({ tid }) { async get({ tid }) {
let [tdoc, rows, udict] = await this.getScoreboard(tid); const [tdoc, rows, udict] = await this.getScoreboard(tid);
let path = [ const path = [
['contest_main', '/c'], ['contest_main', '/c'],
[tdoc.title, `/c/${tid}`, true], [tdoc.title, `/c/${tid}`, true],
['contest_scoreboard', null] ['contest_scoreboard', null],
]; ];
this.response.template = 'contest_scoreboard.html'; this.response.template = 'contest_scoreboard.html';
this.response.body = { tdoc, rows, path, udict }; this.response.body = {
tdoc, rows, path, udict,
};
} }
} }
class ContestEditHandler extends ContestDetailHandler { class ContestEditHandler extends ContestDetailHandler {
async prepare({ tid }) { async prepare({ tid }) {
let tdoc = await contest.get(tid); const tdoc = await contest.get(tid);
if (!tdoc.owner != this.user._id) this.checkPerm(PERM_EDIT_CONTEST); if (!tdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_CONTEST);
} }
async get() { async get() {
this.response.template = 'contest_edit.html'; this.response.template = 'contest_edit.html';
let rules = {}; const rules = {};
for (let i in contest.RULES) for (const i in contest.RULES) { rules[i] = contest.RULES[i].TEXT; }
rules[i] = contest.RULES[i].TEXT; const duration = (this.tdoc.endAt.getTime() - this.tdoc.beginAt.getTime()) / 3600 / 1000;
let duration = (this.tdoc.endAt.getTime() - this.tdoc.beginAt.getTime()) / 3600 / 1000; const path = [
let path = [
['contest_main', '/c'], ['contest_main', '/c'],
[this.tdoc.title, `/c/${this.tdoc._id}`, true], [this.tdoc.title, `/c/${this.tdoc._id}`, true],
['contest_edit', null] ['contest_edit', null],
]; ];
let dt = this.tdoc.beginAt; const dt = this.tdoc.beginAt;
this.response.body = { this.response.body = {
rules, tdoc: this.tdoc, duration, path, pids: this.tdoc.pids.join(','), rules,
tdoc: this.tdoc,
duration,
path,
pids: this.tdoc.pids.join(','),
date_text: `${dt.getFullYear()}-${dt.getMonth() + 1}-${dt.getDate()}`, date_text: `${dt.getFullYear()}-${dt.getMonth() + 1}-${dt.getDate()}`,
time_text: `${dt.getHours()}:${dt.getMinutes()}`, time_text: `${dt.getHours()}:${dt.getMinutes()}`,
page_name: 'contest_edit' page_name: 'contest_edit',
}; };
} }
async post({ beginAtDate, beginAtTime, duration, title, content, rule, pids }) {
let beginAt, endAt; async post({
beginAtDate, beginAtTime, duration, title, content, rule, pids,
}) {
let beginAt;
try { try {
beginAt = new Date(Date.parse(`${beginAtDate} ${beginAtTime.replace('-', ':')}`)); beginAt = new Date(Date.parse(`${beginAtDate} ${beginAtTime.replace('-', ':')}`));
} catch (e) { } catch (e) {
throw new ValidationError('beginAtDate', 'beginAtTime'); throw new ValidationError('beginAtDate', 'beginAtTime');
} }
endAt = new Date(beginAt + duration * 3600 * 1000); const endAt = new Date(beginAt + duration * 3600 * 1000);
if (beginAt >= endAt) throw new ValidationError('duration'); if (beginAt >= endAt) throw new ValidationError('duration');
pids = await this.verifyProblems(pids); pids = await this.verifyProblems(pids);
await contest.edit(this.tdoc._id, title, content, rule, beginAt, endAt, pids); await contest.edit(this.tdoc._id, title, content, rule, beginAt, endAt, pids);
if (this.tdoc.beginAt != beginAt || this.tdoc.endAt != endAt if (this.tdoc.beginAt !== beginAt || this.tdoc.endAt !== endAt
|| Array.isDiff(this.tdoc.pids, pids) || this.tdoc.rule != rule) || Array.isDiff(this.tdoc.pids, pids) || this.tdoc.rule !== rule) {
await contest.recalcStatus(this.tdoc._id); await contest.recalcStatus(this.tdoc._id);
}
if (this.preferJson) this.response.body = { tid: this.tdoc._id }; if (this.preferJson) this.response.body = { tid: this.tdoc._id };
else this.response.redirect = `/c/${this.tdoc._id}`; else this.response.redirect = `/c/${this.tdoc._id}`;
} }
} }
class ContestProblemHandler extends ContestDetailHandler { class ContestProblemHandler extends ContestDetailHandler {
async prepare({ tid, pid }) { async prepare({ tid, pid }) {
[this.tdoc, this.pdoc] = await Promise.all([contest.get(tid), problem.get(pid, this.user._id)]); [this.tdoc, this.pdoc] = await Promise.all([
contest.get(tid),
problem.get(pid, this.user._id),
]);
[this.tsdoc, this.udoc] = await Promise.all([ [this.tsdoc, this.udoc] = await Promise.all([
contest.getStatus(this.tdoc._id, this.user._id), contest.getStatus(this.tdoc._id, this.user._id),
user.getById(this.tdoc.owner) user.getById(this.tdoc.owner),
]); ]);
this.attended = this.tsdoc && this.tsdoc.attend == 1; this.attended = this.tsdoc && this.tsdoc.attend === 1;
this.response.template = 'problem_detail.html'; this.response.template = 'problem_detail.html';
if (contest.is_done(this.tdoc)) { if (contest.is_done(this.tdoc)) {
if (!this.attended) throw new ContestNotAttendedError(this.tdoc._id); if (!this.attended) throw new ContestNotAttendedError(this.tdoc._id);
if (!contest.is_ongoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id); if (!contest.is_ongoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id);
} }
console.log(this.tdoc.pids, this.pdoc._id); console.log(this.tdoc.pids, this.pdoc._id);
if (!this.tdoc.pids.map(s => s.toString()).includes(this.pdoc._id.toString())) if (!this.tdoc.pids.map((s) => s.toString()).includes(this.pdoc._id.toString())) {
throw new ProblemNotFoundError(pid, this.tdoc._id); throw new ProblemNotFoundError(pid, this.tdoc._id);
}
} }
async get({ tid }) { async get({ tid }) {
let path = [ const path = [
['contest_main', '/c'], ['contest_main', '/c'],
[this.tdoc.title, `/c/${tid}`, true], [this.tdoc.title, `/c/${tid}`, true],
[this.pdoc.title, null, true] [this.pdoc.title, null, true],
]; ];
this.response.body = { this.response.body = {
tdoc: this.tdoc, pdoc: this.pdoc, tsdoc: this.tsdoc, tdoc: this.tdoc,
udoc: this.udoc, attended: this.attended, path pdoc: this.pdoc,
tsdoc: this.tsdoc,
udoc: this.udoc,
attended: this.attended,
path,
}; };
} }
} }
class ContestProblemSubmitHandler extends ContestProblemHandler { class ContestProblemSubmitHandler extends ContestProblemHandler {
async get({ tid, pid }) { async get({ tid, pid }) {
let rdocs = []; let rdocs = [];
if (this.canShowRecord(this.tdoc)) if (this.canShowRecord(this.tdoc)) {
rdocs = await record.getUserInProblemMulti(this.user._id, this.pdoc._id, true) rdocs = await record.getUserInProblemMulti(this.user._id, this.pdoc._id, true)
.sort({ _id: -1 }).limit(10).toArray(); .sort({ _id: -1 }).limit(10).toArray();
}
this.response.template = 'problem_submit.html'; this.response.template = 'problem_submit.html';
let path = [ const path = [
['contest_main', '/c'], ['contest_main', '/c'],
[this.tdoc.title, `/c/${tid}`, true], [this.tdoc.title, `/c/${tid}`, true],
[this.pdoc.title, `/c/${tid}/p/${pid}`, true], [this.pdoc.title, `/c/${tid}/p/${pid}`, true],
['contest_detail_problem_submit', null] ['contest_detail_problem_submit', null],
]; ];
this.response.body = { this.response.body = {
tdoc: this.tdoc, pdoc: this.pdoc, tsdoc: this.tsdoc, tdoc: this.tdoc,
udoc: this.udoc, attended: this.attend, path, rdocs pdoc: this.pdoc,
tsdoc: this.tsdoc,
udoc: this.udoc,
attended: this.attend,
path,
rdocs,
}; };
} }
async post({ tid, lang, code }) { async post({ tid, lang, code }) {
await this.limitRate('add_record', 60, 100); await this.limitRate('add_record', 60, 100);
let rid = await record.add({ pid: this.pdoc._id, uid: this.user._id, lang, code, tid: this.tdoc._id, hidden: true }); const rid = await record.add({
pid: this.pdoc._id, uid: this.user._id, lang, code, tid: this.tdoc._id, hidden: true,
});
await contest.updateStatus(this.tdoc._id, this.user._id, rid, this.pdoc._id, false, 0); await contest.updateStatus(this.tdoc._id, this.user._id, rid, this.pdoc._id, false, 0);
if (!this.canShowRecord(this.tdoc)) { if (!this.canShowRecord(this.tdoc)) {
this.response.body = { tid }; this.response.body = { tid };
@@ -188,33 +220,37 @@ class ContestCreateHandler extends ContestHandler {
async prepare() { async prepare() {
this.checkPerm(PERM_CREATE_CONTEST); this.checkPerm(PERM_CREATE_CONTEST);
} }
async get() { async get() {
this.response.template = 'contest_edit.html'; this.response.template = 'contest_edit.html';
let rules = {}; const rules = {};
for (let i in contest.RULES) for (const i in contest.RULES) { rules[i] = contest.RULES[i].TEXT; }
rules[i] = contest.RULES[i].TEXT; const now = new Date();
let now = new Date();
let ts = now.getTime(); let ts = now.getTime();
ts = ts - ts % (15 * 60 * 1000) + 15 * 60 * 1000; ts = ts - (ts % (15 * 60 * 1000)) + 15 * 60 * 1000;
let dt = new Date(ts); const dt = new Date(ts);
this.response.body = { this.response.body = {
rules, page_name: 'contest_create', rules,
page_name: 'contest_create',
date_text: `${dt.getFullYear()}-${dt.getMonth() + 1}-${dt.getDate()}`, date_text: `${dt.getFullYear()}-${dt.getMonth() + 1}-${dt.getDate()}`,
time_text: `${dt.getHours()}:${dt.getMinutes()}`, time_text: `${dt.getHours()}:${dt.getMinutes()}`,
pids: '1000, 1001' pids: '1000, 1001',
}; };
} }
async post({ title, content, rule, beginAtDate, beginAtTime, duration, pids }) {
let beginAt, endAt; async post({
title, content, rule, beginAtDate, beginAtTime, duration, pids,
}) {
let beginAt;
try { try {
beginAt = new Date(Date.parse(`${beginAtDate} ${beginAtTime.replace('-', ':')}`)); beginAt = new Date(Date.parse(`${beginAtDate} ${beginAtTime.replace('-', ':')}`));
} catch (e) { } catch (e) {
throw new ValidationError('beginAtDate', 'beginAtTime'); throw new ValidationError('beginAtDate', 'beginAtTime');
} }
endAt = new Date(beginAt.getTime() + duration * 3600 * 1000); const endAt = new Date(beginAt.getTime() + duration * 3600 * 1000);
if (beginAt >= endAt) throw new ValidationError('duration'); if (beginAt >= endAt) throw new ValidationError('duration');
pids = await this.verifyProblems(pids); pids = await this.verifyProblems(pids);
let tid = await contest.add(title, content, this.user._id, rule, beginAt, endAt, pids); const tid = await contest.add(title, content, this.user._id, rule, beginAt, endAt, pids);
this.response.body = { tid }; this.response.body = { tid };
this.response.redirect = `/c/${tid}`; this.response.redirect = `/c/${tid}`;
} }
+33 -25
View File
@@ -1,16 +1,17 @@
const const { PERM_JUDGE } = require('../permission');
{ PERM_JUDGE } = require('../permission'), const record = require('../model/record');
record = require('../model/record'), const problem = require('../model/problem');
problem = require('../model/problem'), const bus = require('../service/bus');
bus = require('../service/bus'), const queue = require('../service/queue');
queue = require('../service/queue'), const {
{ Route, Handler, Connection, ConnectionHandler } = require('../service/server'); Route, Handler, Connection, ConnectionHandler,
} = require('../service/server');
queue.assert('judge'); queue.assert('judge');
async function next(body) { async function next(body) {
let rdoc = await record.get(body.rid); let rdoc = await record.get(body.rid);
let $set = {}; const $set = {};
if (body.case) { if (body.case) {
rdoc.cases.push(body.case); rdoc.cases.push(body.case);
$set.cases = rdoc.cases; $set.cases = rdoc.cases;
@@ -32,7 +33,7 @@ async function next(body) {
} }
async function end(body) { async function end(body) {
let rdoc = await record.get(body.rid); let rdoc = await record.get(body.rid);
let $set = {}; const $set = {};
if (body.case) { if (body.case) {
rdoc.cases.push(body.case); rdoc.cases.push(body.case);
$set.cases = rdoc.cases; $set.cases = rdoc.cases;
@@ -59,29 +60,33 @@ class JudgeHandler extends Handler {
async prepare() { async prepare() {
this.checkPerm(PERM_JUDGE); this.checkPerm(PERM_JUDGE);
} }
async get({ check = false }) { async get({ check = false }) {
this.response.body = {}; this.response.body = {};
if (check) return; if (check) return;
let rid = await queue.get('judge', false); const rid = await queue.get('judge', false);
if (rid) { if (rid) {
let rdoc = await record.get(rid); const rdoc = await record.get(rid);
let pdoc = await problem.getById(rdoc.pid); const pdoc = await problem.getById(rdoc.pid);
let task = { const task = {
event: 'judge', event: 'judge',
rid, type: 0, rid,
type: 0,
pid: rdoc.pid, pid: rdoc.pid,
data: pdoc.data, data: pdoc.data,
lang: rdoc.lang, lang: rdoc.lang,
code: rdoc.code code: rdoc.code,
}; };
this.response.body = { task }; this.response.body = { task };
} }
} }
async post_next() {
async postNext() {
await next(this.request.body); await next(this.request.body);
this.response.body = {}; this.response.body = {};
} }
async post_end() {
async postEnd() {
await end(this.request.body); await end(this.request.body);
this.response.body = {}; this.response.body = {};
} }
@@ -90,26 +95,29 @@ class JudgeConnectionHandler extends ConnectionHandler {
async prepare() { async prepare() {
this.checkPerm(PERM_JUDGE); this.checkPerm(PERM_JUDGE);
} }
async message(msg) { async message(msg) {
if (msg.key == 'next') await next(msg); if (msg.key === 'next') await next(msg);
else if (msg.key == 'end') { else if (msg.key === 'end') {
await end({ judger: this.user._id, ...msg }); await end({ judger: this.user._id, ...msg });
this.processing = null; this.processing = null;
let rid = await queue.get('judge'); const rid = await queue.get('judge');
let rdoc = await record.get(rid); const rdoc = await record.get(rid);
let pdoc = await problem.getById(rdoc.pid); const pdoc = await problem.getById(rdoc.pid);
let task = { const task = {
event: 'judge', event: 'judge',
rid, type: 0, rid,
type: 0,
pid: rdoc.pid, pid: rdoc.pid,
data: pdoc.data, data: pdoc.data,
lang: rdoc.lang, lang: rdoc.lang,
code: rdoc.code code: rdoc.code,
}; };
this.send({ task }); this.send({ task });
this.processing = task.rid; this.processing = task.rid;
} }
} }
async cleanup() { async cleanup() {
if (this.processing) { if (this.processing) {
await record.reset(this.processing); await record.reset(this.processing);
+121 -89
View File
@@ -1,23 +1,26 @@
const const fs = require('fs');
fs = require('fs'), const paginate = require('../lib/paginate');
paginate = require('../lib/paginate'), const validator = require('../lib/validator');
validator = require('../lib/validator'), const problem = require('../model/problem');
problem = require('../model/problem'), const record = require('../model/record');
record = require('../model/record'), const user = require('../model/user');
user = require('../model/user'), const solution = require('../model/solution');
solution = require('../model/solution'), const system = require('../model/system');
system = require('../model/system'), const { Route, Handler } = require('../service/server');
{ Route, Handler } = require('../service/server'), const gridfs = require('../service/gridfs');
gridfs = require('../service/gridfs'), const queue = require('../service/queue');
queue = require('../service/queue'), const {
{ NoProblemError, ProblemDataNotFoundError, BadRequestError, NoProblemError, ProblemDataNotFoundError, BadRequestError,
SolutionNotFoundError } = require('../error'), SolutionNotFoundError,
{ constants } = require('../options'), } = require('../error');
{ PERM_VIEW_PROBLEM, PERM_VIEW_PROBLEM_HIDDEN, PERM_SUBMIT_PROBLEM, const { constants } = require('../options');
PERM_CREATE_PROBLEM, PERM_READ_PROBLEM_DATA, PERM_EDIT_PROBLEM, const {
PERM_JUDGE, PERM_VIEW_PROBLEM_SOLUTION, PERM_CREATE_PROBLEM_SOLUTION, PERM_VIEW_PROBLEM, PERM_VIEW_PROBLEM_HIDDEN, PERM_SUBMIT_PROBLEM,
PERM_EDIT_PROBLEM_SOLUTION, PERM_DELETE_PROBLEM_SOLUTION, PERM_EDIT_PROBLEM_SOLUTION_REPLY, PERM_CREATE_PROBLEM, PERM_READ_PROBLEM_DATA, PERM_EDIT_PROBLEM,
PERM_REPLY_PROBLEM_SOLUTION } = require('../permission'); PERM_JUDGE, PERM_VIEW_PROBLEM_SOLUTION, PERM_CREATE_PROBLEM_SOLUTION,
PERM_EDIT_PROBLEM_SOLUTION, PERM_DELETE_PROBLEM_SOLUTION, PERM_EDIT_PROBLEM_SOLUTION_REPLY,
PERM_REPLY_PROBLEM_SOLUTION,
} = require('../permission');
queue.assert('judge'); queue.assert('judge');
@@ -25,27 +28,30 @@ class ProblemHandler extends Handler {
async _prepare() { async _prepare() {
this.checkPerm(PERM_VIEW_PROBLEM); this.checkPerm(PERM_VIEW_PROBLEM);
} }
async get({ page = 1, category = null }) { async get({ page = 1, category = null }) {
this.response.template = 'problem_main.html'; this.response.template = 'problem_main.html';
let q = {}; const q = {};
if (category) q.category = category; if (category) q.category = category;
if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false;
let pdocs = await problem.getMany(q, { pid: 1 }, page, constants.PROBLEM_PER_PAGE); const pdocs = await problem.getMany(q, { pid: 1 }, page, constants.PROBLEM_PER_PAGE);
this.response.body = { this.response.body = {
path: [ path: [
['Hydro', '/'], ['Hydro', '/'],
['problem_main', null] ['problem_main', null],
], ],
page, pdocs, category: '' page,
pdocs,
category: '',
}; };
} }
} }
class ProblemRandomHandler extends ProblemHandler { class ProblemRandomHandler extends ProblemHandler {
async get() { async get() {
let q = {}; const q = {};
if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false;
let pid = await problem.random(q); const pid = await problem.random(q);
if (!pid) throw new NoProblemError(); if (!pid) throw new NoProblemError();
this.response.body = { pid }; this.response.body = { pid };
} }
@@ -57,19 +63,20 @@ class ProblemDetailHandler extends ProblemHandler {
this.uid = this.user._id; this.uid = this.user._id;
this.pid = pid; this.pid = pid;
if (pid) this.pdoc = await problem.get(pid, this.uid); if (pid) this.pdoc = await problem.get(pid, this.uid);
if (this.pdoc.hidden && this.pdoc.owner != this.uid) this.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); if (this.pdoc.hidden && this.pdoc.owner !== this.uid) this.checkPerm(PERM_VIEW_PROBLEM_HIDDEN);
if (this.pdoc) this.udoc = await user.getById(this.pdoc.owner); if (this.pdoc) this.udoc = await user.getById(this.pdoc.owner);
this.response.body = { this.response.body = {
pdoc: this.pdoc, pdoc: this.pdoc,
udoc: this.udoc, udoc: this.udoc,
title: (this.pdoc || {}).title || '' title: (this.pdoc || {}).title || '',
}; };
} }
async get() { async get() {
this.response.body.path = [ this.response.body.path = [
['Hydro', '/'], ['Hydro', '/'],
['problem_main', '/p'], ['problem_main', '/p'],
[this.pdoc.title, null, true] [this.pdoc.title, null, true],
]; ];
} }
} }
@@ -78,26 +85,31 @@ class ProblemSubmitHandler extends ProblemDetailHandler {
async prepare() { async prepare() {
this.checkPerm(PERM_SUBMIT_PROBLEM); this.checkPerm(PERM_SUBMIT_PROBLEM);
} }
async get() { async get() {
this.response.template = 'problem_submit.html'; this.response.template = 'problem_submit.html';
let rdocs = await record.getUserInProblemMulti(this.uid, this.pdoc._id).sort({ _id: -1 }).limit(10).toArray(); const rdocs = await record.getUserInProblemMulti(this.uid, this.pdoc._id)
.sort({ _id: -1 })
.limit(10)
.toArray();
this.response.body = { this.response.body = {
path: [ path: [
['Hydro', '/'], ['Hydro', '/'],
['problem_main', '/p'], ['problem_main', '/p'],
[this.pdoc.title, `/p/${this.pid}`, true], [this.pdoc.title, `/p/${this.pid}`, true],
['problem_submit', null] ['problem_submit', null],
], ],
pdoc: this.pdoc, pdoc: this.pdoc,
udoc: this.udoc, udoc: this.udoc,
rdocs, rdocs,
title: this.pdoc.title title: this.pdoc.title,
}; };
} }
async post() { async post() {
let { lang, code } = this.request.body; const { lang, code } = this.request.body;
let rid = await record.add({ const rid = await record.add({
uid: this.uid, lang, code, pid: this.pdoc._id uid: this.uid, lang, code, pid: this.pdoc._id,
}); });
await queue.push('judge', rid); await queue.push('judge', rid);
this.user.nSubmit++; this.user.nSubmit++;
@@ -108,7 +120,7 @@ class ProblemSubmitHandler extends ProblemDetailHandler {
class ProblemManageHandler extends ProblemDetailHandler { class ProblemManageHandler extends ProblemDetailHandler {
async prepare() { async prepare() {
if (this.pdoc.owner != this.uid) this.checkPerm(PERM_EDIT_PROBLEM); if (this.pdoc.owner !== this.uid) this.checkPerm(PERM_EDIT_PROBLEM);
} }
} }
@@ -119,9 +131,10 @@ class ProblemSettingsHandler extends ProblemManageHandler {
['Hydro', '/'], ['Hydro', '/'],
['problem_main', '/p'], ['problem_main', '/p'],
[this.pdoc.title, `/p/${this.pid}`, true], [this.pdoc.title, `/p/${this.pid}`, true],
['problem_settings', null] ['problem_settings', null],
]; ];
} }
async post() { async post() {
// TODO(masnn) // TODO(masnn)
this.back(); this.back();
@@ -135,43 +148,45 @@ class ProblemEditHandler extends ProblemManageHandler {
['Hydro', '/'], ['Hydro', '/'],
['problem_main', '/p'], ['problem_main', '/p'],
[this.pdoc.title, `/p/${this.request.params.pid}`, true], [this.pdoc.title, `/p/${this.request.params.pid}`, true],
['problem_edit', null] ['problem_edit', null],
]; ];
this.response.body.page_name = 'problem_edit'; this.response.body.page_name = 'problem_edit';
} }
async post({ title, content }) { async post({ title, content }) {
let pid = validator.checkPid(this.request.body.pid); const pid = validator.checkPid(this.request.body.pid);
let pdoc = await problem.get(this.params.pid); const pdoc = await problem.get(this.params.pid);
await problem.edit(pdoc._id, { title, content, pid }); await problem.edit(pdoc._id, { title, content, pid });
this.response.redirect = `/p/${pid}`; this.response.redirect = `/p/${pid}`;
} }
} }
class ProblemDataUploadHandler extends ProblemManageHandler { class ProblemDataUploadHandler extends ProblemManageHandler {
async prepare(){ async prepare() {
this.response.template = 'problem_upload.html'; this.response.template = 'problem_upload.html';
} }
async get() { async get() {
if (this.pdoc.data && typeof this.pdoc.data == 'object') { if (this.pdoc.data && typeof this.pdoc.data === 'object') {
let files = await gridfs.find({ _id: this.pdoc.data }).toArray(); const files = await gridfs.find({ _id: this.pdoc.data }).toArray();
this.md5 = files[0].md5; this.md5 = files[0].md5;
} }
this.response.body.md5 = this.md5; this.response.body.md5 = this.md5;
} }
async post() { async post() {
if (!this.request.files.file) throw new BadRequestError(); if (!this.request.files.file) throw new BadRequestError();
const r = fs.createReadStream(this.request.files.file.path); const r = fs.createReadStream(this.request.files.file.path);
let f = gridfs.openUploadStream('data.zip'); const f = gridfs.openUploadStream('data.zip');
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
r.pipe(f); r.pipe(f);
f.once('finish', resolve); f.once('finish', resolve);
f.once('error', reject); f.once('error', reject);
}); });
if (this.pdoc.data && typeof this.pdoc.data == 'object') if (this.pdoc.data && typeof this.pdoc.data === 'object') { gridfs.delete(this.pdoc.data); }
gridfs.delete(this.pdoc.data);
this.pdoc = await problem.edit(this.pdoc._id, { data: f.id }); this.pdoc = await problem.edit(this.pdoc._id, { data: f.id });
if (this.pdoc.data && typeof this.pdoc.data == 'object') { if (this.pdoc.data && typeof this.pdoc.data === 'object') {
let files = await gridfs.find({ _id: this.pdoc.data }).toArray(); const files = await gridfs.find({ _id: this.pdoc.data }).toArray();
this.md5 = files[0].md5; this.md5 = files[0].md5;
} }
this.response.body.md5 = this.md5; this.response.body.md5 = this.md5;
@@ -180,9 +195,9 @@ class ProblemDataUploadHandler extends ProblemManageHandler {
class ProblemDataDownloadHandler extends ProblemDetailHandler { class ProblemDataDownloadHandler extends ProblemDetailHandler {
async get({ pid }) { async get({ pid }) {
if (this.uid != this.pdoc.owner) this.checkPerm([PERM_READ_PROBLEM_DATA, PERM_JUDGE]); if (this.uid !== this.pdoc.owner) this.checkPerm([PERM_READ_PROBLEM_DATA, PERM_JUDGE]);
if (!this.pdoc.data) throw new ProblemDataNotFoundError(pid); if (!this.pdoc.data) throw new ProblemDataNotFoundError(pid);
else if (typeof this.pdoc.data == 'string') this.ctx.setRedirect = this.pdoc.data.split('from:')[1]; else if (typeof this.pdoc.data === 'string') [, this.ctx.setRedirect] = this.pdoc.data.split('from:');
this.response.attachment(`${this.pdoc.title}.zip`); this.response.attachment(`${this.pdoc.title}.zip`);
this.response.body = gridfs.openDownloadStream(this.pdoc.data); this.response.body = gridfs.openDownloadStream(this.pdoc.data);
} }
@@ -192,70 +207,81 @@ class ProblemSolutionHandler extends ProblemDetailHandler {
async get({ page = 1 }) { async get({ page = 1 }) {
this.response.template = 'problem_solution.html'; this.response.template = 'problem_solution.html';
this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION);
let [psdocs, pcount, pscount] = await paginate(solution.getMulti(this.pdoc._id), page, constants.SOLUTION_PER_PAGE); const [psdocs, pcount, pscount] = await paginate(
let uids = [this.pdoc.owner], docids = []; solution.getMulti(this.pdoc._id),
for (let psdoc of psdocs) { page, constants.SOLUTION_PER_PAGE,
);
const uids = [this.pdoc.owner]; const
docids = [];
for (const psdoc of psdocs) {
docids.push(psdoc._id); docids.push(psdoc._id);
uids.push(psdoc.owner); uids.push(psdoc.owner);
if (psdoc.reply.length) if (psdoc.reply.length) { for (const psrdoc of psdoc.reply) uids.push(psrdoc.owner); }
for (let psrdoc of psdoc.reply)
uids.push(psrdoc.owner);
} }
let udict = await user.getList(uids); const udict = await user.getList(uids);
let pssdict = solution.getListStatus(docids, this.uid); const pssdict = solution.getListStatus(docids, this.uid);
let path = [ const path = [
['problem_main', '/p'], ['problem_main', '/p'],
[this.pdoc.title, `/p/${this.pdoc.pid}`, true], [this.pdoc.title, `/p/${this.pdoc.pid}`, true],
['problem_solution', null] ['problem_solution', null],
]; ];
this.response.body = { this.response.body = {
path, psdocs, page, pcount, pscount, udict, pssdict path, psdocs, page, pcount, pscount, udict, pssdict,
}; };
} }
async post({ psid }) { async post({ psid }) {
if (psid) this.psdoc = await solution.get(psid); if (psid) this.psdoc = await solution.get(psid);
} }
async post_submit({ content }) {
async postSubmit({ content }) {
this.checkPerm(PERM_CREATE_PROBLEM_SOLUTION); this.checkPerm(PERM_CREATE_PROBLEM_SOLUTION);
await solution.add(this.pdoc._id, this.uid, content); await solution.add(this.pdoc._id, this.uid, content);
this.back(); this.back();
} }
async post_edit_solution({ content }) {
if (this.psdoc.owner != this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION); async postEditSolution({ content }) {
if (this.psdoc.owner !== this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION);
this.psdoc = await solution.edit(this.psdoc._id, content); this.psdoc = await solution.edit(this.psdoc._id, content);
this.ctx.body.psdoc = this.psdoc; this.ctx.body.psdoc = this.psdoc;
this.back(); this.back();
} }
async post_delete_solution() {
if (this.psdoc.owner != this.uid) this.checkPerm(PERM_DELETE_PROBLEM_SOLUTION); async postDeleteSolution() {
if (this.psdoc.owner !== this.uid) this.checkPerm(PERM_DELETE_PROBLEM_SOLUTION);
await solution.del(this.psdoc._id); await solution.del(this.psdoc._id);
this.back(); this.back();
} }
async post_reply({ psid, content }) {
async postReply({ psid, content }) {
this.checkPerm(PERM_REPLY_PROBLEM_SOLUTION); this.checkPerm(PERM_REPLY_PROBLEM_SOLUTION);
let psdoc = await solution.get(psid); const psdoc = await solution.get(psid);
await solution.reply(psdoc._id, this.uid, content); await solution.reply(psdoc._id, this.uid, content);
} }
async post_edit_reply({ content, psid, psrid }) {
let [psdoc, psrdoc] = await solution.getReply(psid, psrid); async postEditReply({ content, psid, psrid }) {
if ((!psdoc) || psdoc.pid != this.pdoc._id) throw new SolutionNotFoundError(psid); const [psdoc, psrdoc] = await solution.getReply(psid, psrid);
if (psrdoc.owner != this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY); if ((!psdoc) || psdoc.pid !== this.pdoc._id) throw new SolutionNotFoundError(psid);
if (psrdoc.owner !== this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY);
await solution.editReply(psid, psrid, content); await solution.editReply(psid, psrid, content);
} }
async post_delete_reply({ psid, psrid }) {
let [psdoc, psrdoc] = await solution.getReply(psid, psrid); async postDeleteReply({ psid, psrid }) {
if ((!psdoc) || psdoc.pid != this.pdoc._id) throw new SolutionNotFoundError(psid); const [psdoc, psrdoc] = await solution.getReply(psid, psrid);
if (psrdoc.owner != this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY); if ((!psdoc) || psdoc.pid !== this.pdoc._id) throw new SolutionNotFoundError(psid);
if (psrdoc.owner !== this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY);
await solution.delReply(psid, psrid); await solution.delReply(psid, psrid);
this.back(); this.back();
} }
async post_upvote() {
let [psdoc, pssdoc] = await solution.vote(this.psdoc._id, this.uid, 1); async postUpvote() {
const [psdoc, pssdoc] = await solution.vote(this.psdoc._id, this.uid, 1);
this.response.body = { vote: psdoc.vote, user_vote: pssdoc.vote }; this.response.body = { vote: psdoc.vote, user_vote: pssdoc.vote };
if (!this.preferJson) this.back(); if (!this.preferJson) this.back();
} }
async post_downvote() {
let [psdoc, pssdoc] = await solution.vote(this.psdoc._id, this.uid, -1); async postDownvote() {
const [psdoc, pssdoc] = await solution.vote(this.psdoc._id, this.uid, -1);
this.response.body = { vote: psdoc.vote, user_vote: pssdoc.vote }; this.response.body = { vote: psdoc.vote, user_vote: pssdoc.vote };
if (!this.preferJson) this.back(); if (!this.preferJson) this.back();
} }
@@ -264,7 +290,7 @@ class ProblemSolutionHandler extends ProblemDetailHandler {
class ProblemSolutionRawHandler extends ProblemDetailHandler { class ProblemSolutionRawHandler extends ProblemDetailHandler {
async get({ psid }) { async get({ psid }) {
this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION);
let psdoc = await solution.get(psid); const psdoc = await solution.get(psid);
this.response.type = 'text/markdown'; this.response.type = 'text/markdown';
this.response.body = psdoc.content; this.response.body = psdoc.content;
} }
@@ -273,7 +299,7 @@ class ProblemSolutionRawHandler extends ProblemDetailHandler {
class ProblemSolutionReplyRawHandler extends ProblemDetailHandler { class ProblemSolutionReplyRawHandler extends ProblemDetailHandler {
async get({ psid }) { async get({ psid }) {
this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION);
let [psdoc, psrdoc] = await solution.getReply(psid); const [psdoc, psrdoc] = await solution.getReply(psid);
if ((!psdoc) || psdoc.pid != this.pdoc._id) throw new SolutionNotFoundError(psid); if ((!psdoc) || psdoc.pid != this.pdoc._id) throw new SolutionNotFoundError(psid);
this.response.type = 'text/markdown'; this.response.type = 'text/markdown';
this.response.body = psrdoc.content; this.response.body = psrdoc.content;
@@ -288,14 +314,20 @@ class ProblemCreateHandler extends Handler {
path: [ path: [
['Hydro', '/'], ['Hydro', '/'],
['problem_main', '/p'], ['problem_main', '/p'],
['problem_create', null] ['problem_create', null],
], page_name: 'problem_create' ],
page_name: 'problem_create',
}; };
} }
async post({ title, pid, content, hidden }) {
async post({
title, pid, content, hidden,
}) {
validator.checkPid(pid); validator.checkPid(pid);
pid = pid || await system.incPidCounter(); pid = pid || await system.incPidCounter();
await problem.add({ title, content, owner: this.user._id, pid, hidden }); await problem.add({
title, content, owner: this.user._id, pid, hidden,
});
this.response.body = { pid }; this.response.body = { pid };
this.response.redirect = `/p/${pid}/settings`; this.response.redirect = `/p/${pid}/settings`;
} }
@@ -322,7 +354,7 @@ class ProblemPretestHandler(base.Handler):
@base.requirePerm(builtin.PERM_SUBMIT_PROBLEM) @base.requirePerm(builtin.PERM_SUBMIT_PROBLEM)
@base.route_argument @base.route_argument
@base.post_argument @base.post_argument
@base.require_csrf_token @base.require_csrfToken
@base.sanitize @base.sanitize
@base.limitRate('add_record', 60, 100) @base.limitRate('add_record', 60, 100)
async def post(self, *, pid: document.convert_doc_id, lang: str, code: str, async def post(self, *, pid: document.convert_doc_id, lang: str, code: str,
@@ -379,7 +411,7 @@ class ProblemPretestConnection(record_handler.RecordVisibilityMixin, base.Connec
@base.requirePriv(builtin.PRIV_USER_PROFILE) @base.requirePriv(builtin.PRIV_USER_PROFILE)
@base.route_argument @base.route_argument
@base.post_argument @base.post_argument
@base.require_csrf_token @base.require_csrfToken
@base.sanitize @base.sanitize
async def post(self, *, pid: document.convert_doc_id, title: str, content: str): async def post(self, *, pid: document.convert_doc_id, title: str, content: str):
pdoc = await problem.get(self.domainId, pid) pdoc = await problem.get(self.domainId, pid)
@@ -408,4 +440,4 @@ class ProblemStatisticsHandler(base.Handler):
self.render('problem_statistics.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, self.render('problem_statistics.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc,
page_title=pdoc['title'], path_components=path_components) page_title=pdoc['title'], path_components=path_components)
*/ */
+57 -34
View File
@@ -1,53 +1,71 @@
const const { constants } = require('../options');
{ constants } = require('../options'), const {
{ PERM_READ_RECORD_CODE, PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD, PERM_READ_RECORD_CODE, PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD,
PERM_REJUDGE, PERM_VIEW_PROBLEM_HIDDEN } = require('../permission'), PERM_REJUDGE, PERM_VIEW_PROBLEM_HIDDEN,
problem = require('../model/problem'), } = require('../permission');
record = require('../model/record'), const problem = require('../model/problem');
contest = require('../model/contest'), const record = require('../model/record');
user = require('../model/user'), const contest = require('../model/contest');
bus = require('../service/bus'), const user = require('../model/user');
queue = require('../service/queue'), const bus = require('../service/bus');
{ Route, Handler, Connection, ConnectionHandler } = require('../service/server'); const queue = require('../service/queue');
const {
Route, Handler, Connection, ConnectionHandler,
} = require('../service/server');
class RecordListHandler extends Handler { class RecordListHandler extends Handler {
async get({ page = 1 }) { async get({ page = 1 }) {
this.response.template = 'record_main.html'; this.response.template = 'record_main.html';
let q = {}; const q = {};
let rdocs = await record.getMany(q, { _id: -1 }, page, constants.RECORD_PER_PAGE); const rdocs = await record.getMany(q, { _id: -1 }, page, constants.RECORD_PER_PAGE);
let pdict = {}, udict = {}; const pdict = {};
for (let rdoc of rdocs) { const udict = {};
udict[rdoc.uid] = await user.getById(rdoc.uid); const ulist = [];
pdict[rdoc.pid] = await problem.get(rdoc.pid, this.user._id); const plist = [];
for (const rdoc of rdocs) {
ulist[rdoc.uid] = user.getById(rdoc.uid);
plist[rdoc.pid] = problem.get(rdoc.pid, this.user._id);
}
await Promise.all(ulist);
await Promise.all(plist);
for (const udoc of ulist) {
udict[udoc._id] = udoc;
}
for (const pdoc of plist) {
pdict[pdoc._id] = pdoc;
} }
this.response.body = { this.response.body = {
path: [ path: [
['Hydro', '/'], ['Hydro', '/'],
['record_main', null] ['record_main', null],
], ],
page, rdocs, pdict, udict page,
rdocs,
pdict,
udict,
}; };
} }
} }
class RecordDetailHandler extends Handler { class RecordDetailHandler extends Handler {
async get({ rid }) { async get({ rid }) {
this.response.template = 'record_detail.html'; this.response.template = 'record_detail.html';
let rdoc = await record.get(rid); const rdoc = await record.get(rid);
if (rdoc.hidden) this.checkPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); if (rdoc.hidden) this.checkPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
if (rdoc.uid != this.user.uid && !this.user.hasPerm(PERM_READ_RECORD_CODE)) rdoc.code = null; if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM_READ_RECORD_CODE)) rdoc.code = null;
this.response.body = { this.response.body = {
path: [ path: [
['Hydro', '/'], ['Hydro', '/'],
['record_detail', null] ['record_detail', null],
], ],
rdoc, show_status: true rdoc,
show_status: true,
}; };
} }
} }
class RecordRejudgeHandler extends Handler { class RecordRejudgeHandler extends Handler {
async post({ rid }) { async post({ rid }) {
this.checkPerm(PERM_REJUDGE); this.checkPerm(PERM_REJUDGE);
let rdoc = await record.get(rid); const rdoc = await record.get(rid);
if (rdoc) { if (rdoc) {
await record.reset(rid); await record.reset(rid);
await queue.push('judge', rid); await queue.push('judge', rid);
@@ -59,18 +77,21 @@ class RecordConnectionHandler extends ConnectionHandler {
async prepare() { async prepare() {
bus.subscribe(['record_change'], this.onRecordChange); bus.subscribe(['record_change'], this.onRecordChange);
} }
async message(msg) { async message(msg) {
for (let rid of msg.rids) { for (const rid of msg.rids) {
let rdoc = await record.get(rid); const rdoc = await record.get(rid); // eslint-disable-line no-await-in-loop
await this.onRecordChange({ value: rdoc }); await this.onRecordChange({ value: rdoc }); // eslint-disable-line no-await-in-loop
} }
} }
async cleanup() { async cleanup() {
bus.unsubscribe(['record_change'], this.onRecordChange); bus.unsubscribe(['record_change'], this.onRecordChange);
} }
async onRecordChange(data) { async onRecordChange(data) {
let rdoc = data.value; const rdoc = data.value;
if (rdoc.tid && rdoc.tid.toString() != this.tid) return; if (rdoc.tid && rdoc.tid.toString() !== this.tid) return;
let [udoc, pdoc] = await Promise.all([user.getById(rdoc.uid), problem.getById(rdoc.pid)]); let [udoc, pdoc] = await Promise.all([user.getById(rdoc.uid), problem.getById(rdoc.pid)]);
if (pdoc.hidden && !this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null; if (pdoc.hidden && !this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null;
this.send({ html: await this.renderHTML('record_main_tr.html', { rdoc, udoc, pdoc }) }); this.send({ html: await this.renderHTML('record_main_tr.html', { rdoc, udoc, pdoc }) });
@@ -78,9 +99,9 @@ class RecordConnectionHandler extends ConnectionHandler {
} }
class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(ConnectionHandler) { class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(ConnectionHandler) {
async prepare({ rid }) { async prepare({ rid }) {
let rdoc = await record.get(rid); const rdoc = await record.get(rid);
if (rdoc.tid) { if (rdoc.tid) {
let tdoc = await contest.get(rdoc.tid); const tdoc = await contest.get(rdoc.tid);
if (!this.canShowRecord(tdoc)) { if (!this.canShowRecord(tdoc)) {
this.close(); this.close();
return; return;
@@ -90,14 +111,16 @@ class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(Connecti
bus.subscribe(['record_change'], this.onRecordChange); bus.subscribe(['record_change'], this.onRecordChange);
this.onRecordChange({ value: rdoc }); this.onRecordChange({ value: rdoc });
} }
async onRecordChange(data) { async onRecordChange(data) {
let rdoc = data.value; const rdoc = data.value;
if (rdoc._id.toString() != this.rid) return; if (rdoc._id.toString() !== this.rid) return;
this.send({ this.send({
status_html: await this.renderHTML('record_detail_status.html', { rdoc }), status_html: await this.renderHTML('record_detail_status.html', { rdoc }),
summary_html: await this.renderHTML('record_detail_summary.html', { rdoc }) summary_html: await this.renderHTML('record_detail_summary.html', { rdoc }),
}); });
} }
async cleanup() { async cleanup() {
bus.unsubscribe(['record_change'], this.onRecordChange); bus.unsubscribe(['record_change'], this.onRecordChange);
} }
-26
View File
@@ -1,26 +0,0 @@
const
opcount = require('../model/opcount'),
{ PermissionError } = require('../error');
module.exports = {
requirePerm(perm) {
return async (ctx, next) => {
for (let i in arguments) {
if (arguments[i] instanceof Array) {
let p = false;
for (let j in arguments)
if (ctx.state.user.hasPerm(arguments[i][j])) {
p = true;
break;
}
if (!p) throw new PermissionError([arguments[i]]);
} else {
if (ctx.state.user.hasPerm(arguments[i])) continue;
else throw new PermissionError([arguments[i]]);
}
}
await next();
};
},
};
+61 -39
View File
@@ -1,22 +1,24 @@
const const { Route, Handler } = require('../service/server.js');
{ Route, Handler } = require('../service/server.js'), const user = require('../model/user');
user = require('../model/user'), const token = require('../model/token');
token = require('../model/token'), const system = require('../model/system');
system = require('../model/system'), const { sendMail } = require('../lib/mail');
{ sendMail } = require('../lib/mail'), const misc = require('../lib/misc');
misc = require('../lib/misc'), const validator = require('../lib/validator');
validator = require('../lib/validator'), const options = require('../options');
options = require('../options'), const { PERM_REGISTER_USER, PERM_LOGGEDIN } = require('../permission');
{ PERM_REGISTER_USER, PERM_LOGGEDIN } = require('../permission'), const {
{ UserAlreadyExistError, InvalidTokenError, VerifyPasswordError, UserAlreadyExistError, InvalidTokenError, VerifyPasswordError,
UserNotFoundError, LoginError, SystemError } = require('../error'); UserNotFoundError, LoginError, SystemError,
} = require('../error');
class UserLoginHandler extends Handler { class UserLoginHandler extends Handler {
async get() { async get() {
this.response.template = 'user_login.html'; this.response.template = 'user_login.html';
} }
async post({ uname, password, rememberme = false }) { async post({ uname, password, rememberme = false }) {
let udoc = await user.getByUname(uname); const udoc = await user.getByUname(uname);
if (!udoc) throw new LoginError(uname); if (!udoc) throw new LoginError(uname);
if (udoc) udoc.checkPassword(password); if (udoc) udoc.checkPassword(password);
await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip }); await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip });
@@ -25,7 +27,7 @@ class UserLoginHandler extends Handler {
this.session.uid = udoc._id; this.session.uid = udoc._id;
this.session.rememberme = rememberme; this.session.rememberme = rememberme;
this.response.body = {}; this.response.body = {};
let referer = this.request.headers.referer || '/'; const referer = this.request.headers.referer || '/';
this.response.redirect = referer.endsWith('/login') ? '/' : referer; this.response.redirect = referer.endsWith('/login') ? '/' : referer;
} }
} }
@@ -34,9 +36,11 @@ class UserLogoutHandler extends Handler {
super(ctx); super(ctx);
this.checkPerm(PERM_LOGGEDIN); this.checkPerm(PERM_LOGGEDIN);
} }
async get() { async get() {
this.response.template = 'user_logout.html'; this.response.template = 'user_logout.html';
} }
async post() { async post() {
this.session = { uid: 1 }; this.session = { uid: 1 };
this.response.body = {}; this.response.body = {};
@@ -47,16 +51,22 @@ class UserRegisterHandler extends Handler {
super(ctx); super(ctx);
this.checkPerm(PERM_REGISTER_USER); this.checkPerm(PERM_REGISTER_USER);
} }
async get() { async get() {
this.response.template = 'user_register.html'; this.response.template = 'user_register.html';
} }
async post({ mail }) { async post({ mail }) {
validator.checkEmail(mail); validator.checkEmail(mail);
if (await user.getByEmail(mail, true)) throw new UserAlreadyExistError(mail); if (await user.getByEmail(mail, true)) throw new UserAlreadyExistError(mail);
this.limitRate('send_mail', 3600, 30); this.limitRate('send_mail', 3600, 30);
let t = await token.add(token.TYPE_REGISTRATION, options.registration_token_expire_seconds, { mail }); const t = await token.add(
token.TYPE_REGISTRATION,
options.registration_token_expire_seconds,
{ mail },
);
if (options.smtp.user) { if (options.smtp.user) {
let m = await this.renderHTML('user_register_mail', { url: `/register/${t}` }); const m = await this.renderHTML('user_register_mail', { url: `/register/${t}` });
await sendMail(mail, 'Sign Up', 'user_register_mail', m); await sendMail(mail, 'Sign Up', 'user_register_mail', m);
this.response.body = {}; this.response.body = {};
this.response.template = 'user_register_mail_sent.html'; this.response.template = 'user_register_mail_sent.html';
@@ -70,18 +80,24 @@ class UserRegisterWithCodeHandler extends Handler {
super(ctx); super(ctx);
this.checkPerm(PERM_REGISTER_USER); this.checkPerm(PERM_REGISTER_USER);
} }
async get({ code }) { async get({ code }) {
this.response.template = 'user_register_with_code.html'; this.response.template = 'user_register_with_code.html';
let { mail } = await token.get(code, token.TYPE_REGISTRATION); const { mail } = await token.get(code, token.TYPE_REGISTRATION);
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code); if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
this.response.body = { mail }; this.response.body = { mail };
} }
async post({ code, password, verify_password, uname }) {
let { mail } = await token.get(code, token.TYPE_REGISTRATION); async post({
code, password, verifyPassword, uname,
}) {
const { mail } = await token.get(code, token.TYPE_REGISTRATION);
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code); if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
if (password != verify_password) throw new VerifyPasswordError(); if (password !== verifyPassword) throw new VerifyPasswordError();
let uid = await system.incUserCounter(); const uid = await system.incUserCounter();
await user.create({ uid, uname, password, mail, regip: this.request.ip }); await user.create({
uid, uname, password, mail, regip: this.request.ip,
});
await token.delete(code, token.TYPE_REGISTRATION); await token.delete(code, token.TYPE_REGISTRATION);
this.session.uid = uid; this.session.uid = uid;
this.response.body = {}; this.response.body = {};
@@ -93,19 +109,21 @@ class UserLostPassHandler extends Handler {
if (!options.smtp.user) throw new SystemError('Cannot send mail'); if (!options.smtp.user) throw new SystemError('Cannot send mail');
super(ctx); super(ctx);
} }
async get() { async get() {
this.response.template = 'user_lostpass.html'; this.response.template = 'user_lostpass.html';
} }
async post({ mail }) { async post({ mail }) {
validator.checkEmail(mail); validator.checkEmail(mail);
let udoc = await user.getByEmail(mail); const udoc = await user.getByEmail(mail);
if (!udoc) throw new UserNotFoundError(mail); if (!udoc) throw new UserNotFoundError(mail);
let tid = await token.add( const tid = await token.add(
token.TYPE_LOSTPASS, token.TYPE_LOSTPASS,
options.lostpass_token_expire_seconds, options.lostpass_token_expire_seconds,
{ uid: udoc._id } { uid: udoc._id },
); );
let m = await this.renderHTML('user_lostpass_mail', { url: `/lostpass/${tid}`, uname: udoc.uname }); const m = await this.renderHTML('user_lostpass_mail', { url: `/lostpass/${tid}`, uname: udoc.uname });
await sendMail(mail, 'Lost Password', 'user_lostpass_mail', m); await sendMail(mail, 'Lost Password', 'user_lostpass_mail', m);
this.response.body = {}; this.response.body = {};
this.response.template = 'user_lostpass_mail_sent.html'; this.response.template = 'user_lostpass_mail_sent.html';
@@ -113,15 +131,16 @@ class UserLostPassHandler extends Handler {
} }
class UserLostPassWithCodeHandler extends Handler { class UserLostPassWithCodeHandler extends Handler {
async get({ code }) { async get({ code }) {
let tdoc = await token.get(code, token.TYPE_LOSTPASS); const tdoc = await token.get(code, token.TYPE_LOSTPASS);
if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code); if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code);
let udoc = await user.getById(tdoc.uid); const udoc = await user.getById(tdoc.uid);
this.response.body = { uname: udoc.uname }; this.response.body = { uname: udoc.uname };
} }
async post({ code, password, verify_password }) {
let tdoc = await token.get(code, token.TYPE_LOSTPASS); async post({ code, password, verifyPassword }) {
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code); if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code);
if (password != verify_password) throw new VerifyPasswordError(); if (password !== verifyPassword) throw new VerifyPasswordError();
await user.setPassword(tdoc.uid, password); await user.setPassword(tdoc.uid, password);
await token.delete(code, token.TYPE_LOSTPASS); await token.delete(code, token.TYPE_LOSTPASS);
this.response.redirect = '/'; this.response.redirect = '/';
@@ -132,28 +151,31 @@ class UserDetailHandler extends Handler {
super(ctx); super(ctx);
this.response.template = 'user_detail.html'; this.response.template = 'user_detail.html';
} }
async get({ uid }) { async get({ uid }) {
let isSelfProfile = this.ctx.state.user._id == uid; const isSelfProfile = this.ctx.state.user._id === uid;
let udoc = await user.getById(uid); const udoc = await user.getById(uid);
if (!udoc) throw new UserNotFoundError(uid); if (!udoc) throw new UserNotFoundError(uid);
let sdoc = await token.getMostRecentSessionByUid(uid); const sdoc = await token.getMostRecentSessionByUid(uid);
this.ctx.body = { isSelfProfile, udoc, sdoc }; this.ctx.body = { isSelfProfile, udoc, sdoc };
} }
} }
class UserSearchHandler extends Handler { class UserSearchHandler extends Handler {
async get({ q, exact_match = false }) { async get({ q, exactMatch = false }) {
let udocs; let udocs;
if (exact_match) udocs = []; if (exactMatch) udocs = [];
else udocs = await user.getPrefixList(q, 20); else udocs = await user.getPrefixList(q, 20);
try { try {
let udoc = await user.getById(parseInt(q)); const udoc = await user.getById(parseInt(q));
if (udoc) udocs.insert(0, udoc); if (udoc) udocs.insert(0, udoc);
} catch (e) { } catch (e) {
/* Ignore */ /* Ignore */
} }
for (let i in udocs) for (const i in udocs) {
if (udocs[i].gravatar) if (udocs[i].gravatar) {
udocs[i].gravatar_url = misc.gravatar_url[udocs[i].gravatar]; udocs[i].gravatar_url = misc.gravatar_url[udocs[i].gravatar];
}
}
this.ctx.body = { udocs }; this.ctx.body = { udocs };
} }
} }
+27 -28
View File
@@ -1,32 +1,31 @@
require('./utils'); require('./utils');
const const Mongo = require('mongodb');
Mongo = require('mongodb'), const { defaults } = require('lodash');
{ defaults } = require('lodash'), const builtin = require('./model/builtin');
builtin = require('./model/builtin'), const pwhash = require('./lib/pwhash');
pwhash = require('./lib/pwhash'), const options = require('./options');
options = require('./options'), const { udoc } = require('./interface');
{ udoc } = require('./interface');
async function run() { async function run() {
let mongourl = 'mongodb://'; let mongourl = 'mongodb://';
if (options.db.username) mongourl += options.db.username + ':' + options.db.password + '@'; if (options.db.username) mongourl += `${options.db.username}:${options.db.password}@`;
mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`; mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`;
let Database = await Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true }); const Database = await Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true });
let db = Database.db(options.db.name); const db = Database.db(options.db.name);
let coll_user = db.collection('user'); const collUser = db.collection('user');
let coll_role = db.collection('role'); const collRole = db.collection('role');
let coll_blacklist = db.collection('blacklist'); const collBlacklist = db.collection('blacklist');
let coll_token = db.collection('token'); const collToken = db.collection('token');
async function createUser() { async function createUser() {
let salt = pwhash.salt(); const salt = pwhash.salt();
await coll_user.insertMany([ await collUser.insertMany([
defaults({ defaults({
_id: 0, _id: 0,
uname: 'Hydro', uname: 'Hydro',
unameLower: 'hydro', unameLower: 'hydro',
mail: 'hydro@hydro', mail: 'hydro@hydro',
mailLower: 'hydro@hydro', mailLower: 'hydro@hydro',
role: 'guest' role: 'guest',
}, udoc), }, udoc),
defaults({ defaults({
_id: 1, _id: 1,
@@ -34,7 +33,7 @@ async function run() {
mailLower: 'guest@hydro', mailLower: 'guest@hydro',
uname: 'Guest', uname: 'Guest',
unameLower: 'guest', unameLower: 'guest',
role: 'guest' role: 'guest',
}, udoc), }, udoc),
defaults({ defaults({
_id: -1, _id: -1,
@@ -45,22 +44,22 @@ async function run() {
hash: pwhash.hash('rootroot', salt), hash: pwhash.hash('rootroot', salt),
salt, salt,
gravatar: 'root@hydro', gravatar: 'root@hydro',
role: 'admin' role: 'admin',
}, udoc) }, udoc),
]); ]);
} }
await coll_user.createIndex('unameLower', { unique: true }); await collUser.createIndex('unameLower', { unique: true });
await coll_user.createIndex('mailLower', { sparse: true }); await collUser.createIndex('mailLower', { sparse: true });
await coll_role.insertMany(builtin.BUILTIN_ROLES); await collRole.insertMany(builtin.BUILTIN_ROLES);
await coll_blacklist.createIndex('expireAt', { expireAfterSeconds: 0 }); await collBlacklist.createIndex('expireAt', { expireAfterSeconds: 0 });
await coll_token.createIndex([{ uid: 1 }, { tokenType: 1 }, { updateAt: -1 }], { sparse: true }); await collToken.createIndex([{ uid: 1 }, { tokenType: 1 }, { updateAt: -1 }], { sparse: true });
await coll_token.createIndex('expireAt', { expireAfterSeconds: 0 }); await collToken.createIndex('expireAt', { expireAfterSeconds: 0 });
await createUser(); await createUser();
console.log('Installed'); console.log('Installed');
process.exit(0); process.exit(0);
} }
run().catch(e => { run().catch((e) => {
console.error(e); console.error(e);
process.exit(1); process.exit(1);
}); });
+2 -2
View File
@@ -16,7 +16,7 @@ exports.udoc = {
regip: '0.0.0.0', regip: '0.0.0.0',
gravatar: '', gravatar: '',
loginat: new Date(), loginat: new Date(),
loginip: '0.0.0.0' loginip: '0.0.0.0',
}; };
/* /*
export interface Pdoc { export interface Pdoc {
@@ -68,4 +68,4 @@ export interface Tdoc {
content: string content: string
pids: ObjectID[] pids: ObjectID[]
} }
*/ */
+24 -21
View File
@@ -1,37 +1,40 @@
const const fs = require('fs');
fs = require('fs'), const yaml = require('js-yaml');
yaml = require('js-yaml');
let locales = {}; const locales = {};
String.prototype.format = function (args) { String.prototype.format = function formatStr(...args) {
var result = this; let result = this;
if (arguments.length > 0) { if (args.length > 0) {
if (arguments.length == 1 && typeof (args) == 'object') { if (args.length === 1 && typeof (args[0]) === 'object') {
for (var key in args) for (const key in args) {
if (args[key] != undefined) { if (args[key] !== undefined) {
let reg = new RegExp('(\\{' + key + '\\})', 'g'); const reg = new RegExp(`(\\{${key}\\})`, 'g');
result = result.replace(reg, args[key]); result = result.replace(reg, args[key]);
} }
} else for (var i = 0; i < arguments.length; i++)
if (arguments[i] != undefined) {
let reg = new RegExp('(\\{)' + i + '(\\})', 'g');
result = result.replace(reg, arguments[i]);
} }
} else {
for (let i = 0; i < args.length; i++) {
if (args[i] !== undefined) {
const reg = new RegExp(`(\\{)${i}(\\})`, 'g');
result = result.replace(reg, args[i]);
}
}
}
} }
return result; return result;
}; };
String.prototype.rawformat = function (object) { String.prototype.rawformat = function rawFormat(object) {
let res = this.split('{@}'); const res = this.split('{@}');
return [res[0], object, res[1]]; return [res[0], object, res[1]];
}; };
String.prototype.translate = function (language = 'zh_CN') { String.prototype.translate = function translate(language = 'zh_CN') {
if (locales[language]) return locales[language][this] || this; if (locales[language]) return locales[language][this] || this;
else return this; return this;
}; };
module.exports = function load(file, language) { module.exports = function load(file, language) {
if (!locales[language]) locales[language] = {}; if (!locales[language]) locales[language] = {};
let content = fs.readFileSync(file).toString(); const content = fs.readFileSync(file).toString();
Object.assign(locales[language], yaml.safeLoad(content)); Object.assign(locales[language], yaml.safeLoad(content));
}; };
+13 -11
View File
@@ -1,7 +1,6 @@
const const nodemailer = require('nodemailer');
nodemailer = require('nodemailer'), const options = require('../options');
options = require('../options'), const { SendMailError } = require('../error');
{ SendMailError } = require('../error');
let transporter = null; let transporter = null;
try { try {
@@ -11,8 +10,8 @@ try {
secure: options.VJ_SMTP_SECURE, secure: options.VJ_SMTP_SECURE,
auth: { auth: {
user: options.VJ_SMTP_USER, user: options.VJ_SMTP_USER,
pass: options.VJ_SMTP_PASSWORD pass: options.VJ_SMTP_PASSWORD,
} },
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -20,8 +19,8 @@ try {
module.exports = { module.exports = {
/** /**
* @param {string} to * @param {string} to
* @param {string} subject * @param {string} subject
* @param {string} text * @param {string} text
* @param {string} html * @param {string} html
*/ */
@@ -31,7 +30,10 @@ module.exports = {
t = await new Promise((resolve, reject) => { t = await new Promise((resolve, reject) => {
transporter.sendMail({ transporter.sendMail({
from: options.VJ_SMTP_FROM, from: options.VJ_SMTP_FROM,
to, subject, text, html to,
subject,
text,
html,
}, (err, info) => { }, (err, info) => {
if (err) reject(err); if (err) reject(err);
else resolve(info); else resolve(info);
@@ -41,5 +43,5 @@ module.exports = {
throw new SendMailError(to); throw new SendMailError(to);
} }
return t; return t;
} },
}; };
+7 -7
View File
@@ -1,19 +1,19 @@
const const hljs = require('highlight.js');
hljs = require('highlight.js'), const MarkdownIt = require('markdown-it');
MarkdownIt = require('markdown-it'), const katex = require('markdown-it-katex');
katex = require('markdown-it-katex');
class Markdown extends MarkdownIt { class Markdown extends MarkdownIt {
constructor() { constructor() {
super({ super({
linkify: true, linkify: true,
highlight: function (str, lang) { highlight(str, lang) {
if (lang && hljs.getLanguage(lang)) if (lang && hljs.getLanguage(lang)) {
try { try {
return hljs.highlight(lang, str).value; return hljs.highlight(lang, str).value;
} catch (__) { } // eslint-disable-line no-empty } catch (__) { } // eslint-disable-line no-empty
}
return ''; return '';
} },
}); });
this.linkify.tlds('.py', false); this.linkify.tlds('.py', false);
this.use(katex); this.use(katex);
+27 -31
View File
@@ -1,8 +1,7 @@
/* eslint-disable no-bitwise */
'use strict';
function safeAdd(x, y) { function safeAdd(x, y) {
var lsw = (x & 0xffff) + (y & 0xffff); const lsw = (x & 0xffff) + (y & 0xffff);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16); const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xffff); return (msw << 16) | (lsw & 0xffff);
} }
const bitRotateLeft = (num, cnt) => (num << cnt) | (num >>> (32 - cnt)); const bitRotateLeft = (num, cnt) => (num << cnt) | (num >>> (32 - cnt));
@@ -14,15 +13,15 @@ const md5ii = (a, b, c, d, x, s, t) => md5cmn(c ^ (b | ~d), a, b, x, s, t);
function binlMD5(x, len) { function binlMD5(x, len) {
x[len >> 5] |= 0x80 << len % 32; x[len >> 5] |= 0x80 << len % 32;
x[(((len + 64) >>> 9) << 4) + 14] = len; x[(((len + 64) >>> 9) << 4) + 14] = len;
var i; let i;
var olda; let olda;
var oldb; let oldb;
var oldc; let oldc;
var oldd; let oldd;
var a = 1732584193; let a = 1732584193;
var b = -271733879; let b = -271733879;
var c = -1732584194; let c = -1732584194;
var d = 271733878; let d = 271733878;
for (i = 0; i < x.length; i += 16) { for (i = 0; i < x.length; i += 16) {
olda = a; olda = a;
oldb = b; oldb = b;
@@ -100,34 +99,31 @@ function binlMD5(x, len) {
return [a, b, c, d]; return [a, b, c, d];
} }
function binl2rstr(input) { function binl2rstr(input) {
var i; let i;
var output = ''; let output = '';
var length32 = input.length * 32; const length32 = input.length * 32;
for (i = 0; i < length32; i += 8) for (i = 0; i < length32; i += 8) { output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff); }
output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff);
return output; return output;
} }
function rstr2binl(input) { function rstr2binl(input) {
var i; let i;
var output = []; const output = [];
output[(input.length >> 2) - 1] = undefined; output[(input.length >> 2) - 1] = undefined;
for (i = 0; i < output.length; i += 1) for (i = 0; i < output.length; i += 1) { output[i] = 0; }
output[i] = 0; const length8 = input.length * 8;
var length8 = input.length * 8; for (i = 0; i < length8; i += 8) output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32;
for (i = 0; i < length8; i += 8)
output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32;
return output; return output;
} }
const rstrMD5 = s => binl2rstr(binlMD5(rstr2binl(s), s.length * 8)); const rstrMD5 = (s) => binl2rstr(binlMD5(rstr2binl(s), s.length * 8));
function rstr2hex(input) { function rstr2hex(input) {
var hexTab = '0123456789abcdef'; const hexTab = '0123456789abcdef';
var output = ''; let output = '';
var x; let x;
var i; let i;
for (i = 0; i < input.length; i += 1) { for (i = 0; i < input.length; i += 1) {
x = input.charCodeAt(i); x = input.charCodeAt(i);
output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f); output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f);
} }
return output; return output;
} }
module.exports = s => rstr2hex(rstrMD5(unescape(encodeURIComponent(s)))); module.exports = (s) => rstr2hex(rstrMD5(unescape(encodeURIComponent(s))));
+23 -20
View File
@@ -1,41 +1,44 @@
const md5 = require('./md5'); const md5 = require('./md5');
exports.gravatar_url = function (email, size) {
exports.gravatar_url = function gravatarUrl(email, size) {
return `//gravatar.loli.net/avatar/${md5((email || '').toString().trim().toLowerCase())}?d=mm&s=${size}`; return `//gravatar.loli.net/avatar/${md5((email || '').toString().trim().toLowerCase())}?d=mm&s=${size}`;
}; };
exports.datetime_span = function (dt, relative = true, format = '%Y-%m-%d %H:%M:%S') { exports.datetime_span = function datetimeSpan(dt, relative = true, format = '%Y-%m-%d %H:%M:%S') {
if (dt.generationTime) dt = new Date(dt.generationTime * 1000); if (dt.generationTime) dt = new Date(dt.generationTime * 1000);
else if (typeof dt == 'number' || typeof dt == 'string') dt = new Date(dt); else if (typeof dt === 'number' || typeof dt === 'string') dt = new Date(dt);
return '<span class="time{0}" data-timestamp="{1}">{2}</span>'.format( return '<span class="time{0}" data-timestamp="{1}">{2}</span>'.format(
relative ? ' relative' : '', relative ? ' relative' : '',
dt.getTime() / 1000, dt.getTime() / 1000,
dt.format(format) dt.format(format),
); );
}; };
exports.paginate = function* (page, num_pages) { exports.paginate = function* paginate(page, numPages) {
let radius = 2, first, last; const radius = 2; let first; let
last;
if (page > 1) { if (page > 1) {
yield ['first', 1]; yield ['first', 1];
yield ['previous', page - 1]; yield ['previous', page - 1];
} }
if (page <= radius) [first, last] = [1, Math.min(1 + radius * 2, num_pages)]; if (page <= radius) [first, last] = [1, Math.min(1 + radius * 2, numPages)];
else if (page >= num_pages - radius) [first, last] = [Math.max(1, num_pages - radius * 2), num_pages]; else if (page >= numPages - radius) {
else[first, last] = [page - radius, page + radius]; [first, last] = [Math.max(1, numPages - radius * 2), numPages];
} else [first, last] = [page - radius, page + radius];
if (first > 1) yield ['ellipsis', 0]; if (first > 1) yield ['ellipsis', 0];
for (let page0 = first; page0 < last + 1; page0++) { for (let page0 = first; page0 < last + 1; page0++) {
if (page0 != page) yield ['page', page0]; if (page0 !== page) yield ['page', page0];
else yield ['current', page]; else yield ['current', page];
} }
if (last < num_pages) yield ['ellipsis', 0]; if (last < numPages) yield ['ellipsis', 0];
if (page < num_pages) yield ['next', page + 1]; if (page < numPages) yield ['next', page + 1];
yield ['last', num_pages]; yield ['last', numPages];
}; };
exports.format_size = function (size, base = 1) { exports.format_size = function formatSize(size, base = 1) {
size *= base; size *= base;
let unit = 1024; const unit = 1024;
let unit_names = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; const unitNames = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
for (let unit_name of unit_names) { for (const unitName of unitNames) {
if (size < unit) return '{0} {1}'.format(Math.round(size), unit_name); if (size < unit) return '{0} {1}'.format(Math.round(size), unitName);
size /= unit; size /= unit;
} }
return '{0} {1}'.format(Math.round(size * unit), unit_names[unit_names.length - 1]); return '{0} {1}'.format(Math.round(size * unit), unitNames[unitNames.length - 1]);
}; };
+6 -5
View File
@@ -1,10 +1,11 @@
const { ValidationError } = require('../error'); const { ValidationError } = require('../error');
module.exports = async function paginate(cursor, page, page_size) {
module.exports = async function paginate(cursor, page, pageSize) {
if (page <= 0) throw new ValidationError('page'); if (page <= 0) throw new ValidationError('page');
let [count, page_docs] = await Promise.all([ const [count, pageDocs] = await Promise.all([
cursor.count(), cursor.count(),
cursor.skip((page - 1) * page_size).limit(page_size).toArray() cursor.skip((page - 1) * pageSize).limit(pageSize).toArray(),
]); ]);
let num_pages = Math.floor((count + page_size - 1) / page_size); const numPages = Math.floor((count + pageSize - 1) / pageSize);
return [page_docs, num_pages, count]; return [pageDocs, numPages, count];
}; };
+9 -8
View File
@@ -1,26 +1,27 @@
const crypto = require('crypto'); const crypto = require('crypto');
module.exports = { module.exports = {
_b64encode: str => new Buffer(str).toString('base64'), _b64encode: (str) => Buffer.from(str).toString('base64'),
_b64decode: str => new Buffer(str, 'base64').toString(), _b64decode: (str) => Buffer.from(str, 'base64').toString(),
_md5: str => crypto.createHash('md5').update(str).digest('hex'), _md5: (str) => crypto.createHash('md5').update(str).digest('hex'),
_sha1: str => crypto.createHash('sha1').update(str).digest('hex'), _sha1: (str) => crypto.createHash('sha1').update(str).digest('hex'),
/** /**
* @param {string} password * @param {string} password
* @param {string} salt * @param {string} salt
* @param {string} hash * @param {string} hash
*/ */
check(password, salt, hash) { check(password, salt, hash) {
return hash == this.hash(password, salt); return hash === this.hash(password, salt);
}, },
/** /**
* @param {string} password * @param {string} password
* @param {string} salt * @param {string} salt
*/ */
hash(password, salt) { hash(password, salt) {
let res = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha256'); const res = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha256');
return res.toString('Hex'); return res.toString('Hex');
}, },
salt() { salt() {
return String.random(); return String.random();
} },
}; };
+7 -7
View File
@@ -1,11 +1,11 @@
module.exports = function* ranked(diter, equ = (a, b) => a == b) { module.exports = function* ranked(diter, equ = (a, b) => a === b) {
let last_doc = null; let last = null;
let r = 0, count = 0; let r = 0;
for (let doc of diter) { let count = 0;
for (const doc of diter) {
count++; count++;
if (count == 1 || !equ(last_doc, doc)) if (count === 1 || !equ(last, doc)) r = count;
r = count; last = doc;
last_doc = doc;
yield [r, doc]; yield [r, doc];
} }
}; };
+17 -24
View File
@@ -1,34 +1,27 @@
const const path = require('path');
path = require('path'), const nunjucks = require('nunjucks');
nunjucks = require('nunjucks'), const markdown = require('./markdown');
markdown = require('./markdown'), const options = require('../options');
options = require('../options'), const perm = require('../permission');
perm = require('../permission'), const builtin = require('../model/builtin');
builtin = require('../model/builtin'), const model = require('../model');
model = require('../model'), const misc = require('./misc');
misc = require('../lib/misc');
class Nunjucks extends nunjucks.Environment { class Nunjucks extends nunjucks.Environment {
constructor() { constructor() {
super( super(
new nunjucks.FileSystemLoader(path.resolve(options.template.path)), new nunjucks.FileSystemLoader(path.resolve(options.template.path)),
{ autoescape: true, trimBlocks: true } { autoescape: true, trimBlocks: true },
); );
this.addFilter('json', function (self) { this.addFilter('json', (self) => JSON.stringify(self), false);
return JSON.stringify(self); this.addFilter('assign', (self, data) => Object.assign(self, data));
}, false); this.addFilter('markdown', (self) => markdown.render(self));
this.addFilter('assign', function (self, data) {
return Object.assign(self, data);
});
this.addFilter('markdown', function (self) {
return markdown.render(self);
});
this.addFilter('gravatar_url', misc.gravatar_url); this.addFilter('gravatar_url', misc.gravatar_url);
this.addFilter('format_size', misc.format_size); this.addFilter('format_size', misc.format_size);
this.addGlobal('typeof', o => typeof o); this.addGlobal('typeof', (o) => typeof o);
this.addGlobal('console', console); this.addGlobal('console', console);
this.addGlobal('static_url', str => `/${str}`); this.addGlobal('static_url', (str) => `/${str}`);
this.addGlobal('reverse_url', str => str); this.addGlobal('reverse_url', (str) => str);
this.addGlobal('datetime_span', misc.datetime_span); this.addGlobal('datetime_span', misc.datetime_span);
this.addGlobal('paginate', misc.paginate); this.addGlobal('paginate', misc.paginate);
this.addGlobal('perm', perm); this.addGlobal('perm', perm);
@@ -36,6 +29,6 @@ class Nunjucks extends nunjucks.Environment {
this.addGlobal('model', model); this.addGlobal('model', model);
} }
} }
let env = new Nunjucks(); const env = new Nunjucks();
module.exports = env; module.exports = env;
+39 -32
View File
@@ -1,37 +1,44 @@
const const { ValidationError } = require('../error');
{ ValidationError } = require('../error'),
RE_UID = /^-?\d+$/i,
RE_PID = /^([a-zA-Z]+[a-zA-Z0-9]*)|$/i,
RE_UNAME = /[^\s\u3000](.{,254}[^\s\u3000])?$/i,
RE_MAIL = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i;
const const RE_UID = /^-?\d+$/i;
isTitle = s => s && s.length < 64, const RE_PID = /^([a-zA-Z]+[a-zA-Z0-9]*)|$/i;
checkTitle = s => { if (!isTitle(s)) throw new ValidationError('title'); else return s; }, const RE_UNAME = /[^\s\u3000](.{,254}[^\s\u3000])?$/i;
isUid = s => RE_UID.test(s), const RE_MAIL = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i;
checkUid = s => { if (!isUid(s)) throw new ValidationError('uid'); else return s; },
isUname = s => RE_UNAME.test(s), const isTitle = (s) => s && s.length < 64;
checkUname = s => { if (!isUname(s)) throw new ValidationError('uname'); else return s; }, const checkTitle = (s) => { if (!isTitle(s)) throw new ValidationError('title'); else return s; };
isPassword = s => s.length >= 5, const isUid = (s) => RE_UID.test(s);
checkPassword = s => { if (!isPassword(s)) throw new ValidationError('password'); else return s; }, const checkUid = (s) => { if (!isUid(s)) throw new ValidationError('uid'); else return s; };
isEmail = s => RE_MAIL.test(s), const isUname = (s) => RE_UNAME.test(s);
checkEmail = s => { if (!isEmail(s)) throw new ValidationError('mail'); else return s; }, const checkUname = (s) => { if (!isUname(s)) throw new ValidationError('uname'); else return s; };
isContent = s => s && s.length < 65536, const isPassword = (s) => s.length >= 5;
checkContent = s => { if (!isContent(s)) throw new ValidationError('content'); else return s; }, const checkPassword = (s) => { if (!isPassword(s)) throw new ValidationError('password'); else return s; };
isName = s => s && s.length < 256, const isEmail = (s) => RE_MAIL.test(s);
checkName = s => { if (!isName(s)) throw new ValidationError('name'); else return s; }, const checkEmail = (s) => { if (!isEmail(s)) throw new ValidationError('mail'); else return s; };
isPid = s => RE_PID.test(s.toString()), const isContent = (s) => s && s.length < 65536;
checkPid = s => { if (!RE_PID.test(s)) throw new ValidationError('pid'); else return s; }; const checkContent = (s) => { if (!isContent(s)) throw new ValidationError('content'); else return s; };
const isName = (s) => s && s.length < 256;
const checkName = (s) => { if (!isName(s)) throw new ValidationError('name'); else return s; };
const isPid = (s) => RE_PID.test(s.toString());
const checkPid = (s) => { if (!RE_PID.test(s)) throw new ValidationError('pid'); else return s; };
module.exports = { module.exports = {
isTitle, checkTitle, isTitle,
isUid, checkUid, checkTitle,
isUname, checkUname, isUid,
isPassword, checkPassword, checkUid,
isEmail, checkEmail, isUname,
isContent, checkContent, checkUname,
isName, checkName, isPassword,
isPid, checkPid checkPassword,
isEmail,
checkEmail,
isContent,
checkContent,
isName,
checkName,
isPid,
checkPid,
}; };
/* /*
ID_RE = re.compile(r'[^\\/\s\u3000]([^\\/\n\r]*[^\\/\s\u3000])?') ID_RE = re.compile(r'[^\\/\s\u3000]([^\\/\n\r]*[^\\/\s\u3000])?')
@@ -104,4 +111,4 @@ def check_domain_invitation_code(s):
if not is_domain_invitation_code(s): if not is_domain_invitation_code(s):
raise error.ValidationError('invitation_code') raise error.ValidationError('invitation_code')
*/ */
+4 -4
View File
@@ -1,9 +1,9 @@
const const db = require('../service/db.js');
db = require('../service/db.js'),
coll = db.collection('blacklist'); const coll = db.collection('blacklist');
async function add(ip) { async function add(ip) {
let expireAt = new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000); const expireAt = new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000);
return coll.findOneAndUpdate({ _id: ip }, { $set: { expireAt } }, { upsert: true }); return coll.findOneAndUpdate({ _id: ip }, { $set: { expireAt } }, { upsert: true });
} }
function get(ip) { function get(ip) {
+49 -40
View File
@@ -1,20 +1,21 @@
const perm = require('../permission'); const perm = require('../permission');
const BUILTIN_ROLES = [ const BUILTIN_ROLES = [
{ _id: 'guest', perm: perm.PERM_BASIC }, { _id: 'guest', perm: perm.PERM_BASIC },
{ _id: 'default', perm: perm.PERM_DEFAULT }, { _id: 'default', perm: perm.PERM_DEFAULT },
{ _id: 'admin', perm: perm.PERM_ADMIN } { _id: 'admin', perm: perm.PERM_ADMIN },
]; ];
const CATEGORIES = { const CATEGORIES = {
'动态规划': [ 动态规划: [
'LCS', 'LCS',
'LIS', 'LIS',
'背包', '背包',
'单调性DP', '单调性DP',
'环形DP', '环形DP',
'树形DP', '树形DP',
'状态压缩DP' '状态压缩DP',
], ],
'搜索': [ 搜索: [
'枚举', '枚举',
'搜索与剪枝', '搜索与剪枝',
'启发式搜索', '启发式搜索',
@@ -22,9 +23,9 @@ const CATEGORIES = {
'双向搜索', '双向搜索',
'折半搜索', '折半搜索',
'记忆化搜索', '记忆化搜索',
'模拟退火' '模拟退火',
], ],
'计算几何': [ 计算几何: [
'半平面交', '半平面交',
'凸包', '凸包',
'几何图形的交与并', '几何图形的交与并',
@@ -37,19 +38,19 @@ const CATEGORIES = {
'平面图的对偶图', '平面图的对偶图',
'三角剖分', '三角剖分',
'梯形剖分', '梯形剖分',
'几何知识' '几何知识',
], ],
'贪心': [], 贪心: [],
'树结构': [ 树结构: [
'最近公共祖先', '最近公共祖先',
'生成树', '生成树',
'DFS序列', 'DFS序列',
'树上倍增', '树上倍增',
'树的分治', '树的分治',
'树链剖分', '树链剖分',
'Link-Cut-Tree' 'Link-Cut-Tree',
], ],
'图结构': [ 图结构: [
'平面图', '平面图',
'二分图', '二分图',
'二分图匹配', '二分图匹配',
@@ -60,9 +61,9 @@ const CATEGORIES = {
'强连通分量', '强连通分量',
'割点割边', '割点割边',
'欧拉回路', '欧拉回路',
'2-SAT' '2-SAT',
], ],
'数论': [ 数论: [
'素数判定', '素数判定',
'欧几里得算法', '欧几里得算法',
'不定方程', '不定方程',
@@ -73,10 +74,10 @@ const CATEGORIES = {
'大整数质因数分解', '大整数质因数分解',
'勾股方程', '勾股方程',
'积性函数', '积性函数',
'Fibonacci数列' 'Fibonacci数列',
], ],
'模拟': [], 模拟: [],
'数据结构': [ 数据结构: [
'栈', '栈',
'队列', '队列',
'链表', '链表',
@@ -93,41 +94,41 @@ const CATEGORIES = {
'k-d树', 'k-d树',
'块状链表', '块状链表',
'Hashing', 'Hashing',
'函数式编程' '函数式编程',
], ],
'博弈论': [], 博弈论: [],
'字符串': [ 字符串: [
'KMP', 'KMP',
'后缀数据结构', '后缀数据结构',
'Trie树', 'Trie树',
'AC自动机', 'AC自动机',
'Manacher', 'Manacher',
'表达式处理', '表达式处理',
'最小表示法' '最小表示法',
], ],
'组合数学': [ 组合数学: [
'生成函数', '生成函数',
'容斥原理', '容斥原理',
'康托展开', '康托展开',
'Catalan数列', 'Catalan数列',
'Stirling数', 'Stirling数',
'差分', '差分',
'Polya定理' 'Polya定理',
], ],
'线性代数': [ 线性代数: [
'矩阵乘法', '矩阵乘法',
'高斯消元', '高斯消元',
'线性规划' '线性规划',
], ],
'高精度': [ 高精度: [
'FFT' 'FFT',
], ],
'递推': [], 递推: [],
'概率论': [ 概率论: [
'随机化' '随机化',
], ],
'NPC': [], NPC: [],
'其他': [ 其他: [
'二分查找', '二分查找',
'三分查找', '三分查找',
'双指针扫描', '双指针扫描',
@@ -137,15 +138,15 @@ const CATEGORIES = {
'快速幂', '快速幂',
'数学', '数学',
'排序', '排序',
'构造' '构造',
] ],
}; };
const FOOTER_EXTRA_HTMLS = []; const FOOTER_EXTRA_HTMLS = [];
const VIEW_LANGS = [ const VIEW_LANGS = [
{ code: 'zh-CN', name: 'zh-CN' } { code: 'zh-CN', name: 'zh-CN' },
]; ];
const LANGS = { const LANGS = {
cc: 'cpp' cc: 'cpp',
}; };
const LANG_TEXTS = { const LANG_TEXTS = {
c: 'C', c: 'C',
@@ -192,7 +193,7 @@ const STATUS = {
STATUS_JUDGING: 20, STATUS_JUDGING: 20,
STATUS_COMPILING: 21, STATUS_COMPILING: 21,
STATUS_FETCHED: 22, STATUS_FETCHED: 22,
STATUS_IGNORED: 30 STATUS_IGNORED: 30,
}; };
const STATUS_TEXTS = { const STATUS_TEXTS = {
[STATUS.STATUS_WAITING]: 'Waiting', [STATUS.STATUS_WAITING]: 'Waiting',
@@ -226,9 +227,17 @@ const STATUS_CODES = {
20: 'progress', 20: 'progress',
21: 'progress', 21: 'progress',
22: 'progress', 22: 'progress',
30: 'ignored' 30: 'ignored',
}; };
module.exports = { module.exports = {
BUILTIN_ROLES, CATEGORIES, VIEW_LANGS, FOOTER_EXTRA_HTMLS, LANGS, BUILTIN_ROLES,
LANG_TEXTS, LANG_HIGHLIGHT_ID, STATUS, STATUS_TEXTS, STATUS_CODES CATEGORIES,
}; VIEW_LANGS,
FOOTER_EXTRA_HTMLS,
LANGS,
LANG_TEXTS,
LANG_HIGHLIGHT_ID,
STATUS,
STATUS_TEXTS,
STATUS_CODES,
};
+84 -64
View File
@@ -1,18 +1,20 @@
const const user = require('./user');
user = require('./user'), const problem = require('./problem');
problem = require('./problem'), const {
{ ValidationError, ContestNotFoundError, ContestAlreadyAttendedError, ValidationError, ContestNotFoundError, ContestAlreadyAttendedError,
ContestNotAttendedError, ProblemNotFoundError, ContestScoreboardHiddenError } = require('../error'), ContestNotAttendedError, ProblemNotFoundError, ContestScoreboardHiddenError,
{ PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD } = require('../permission'), } = require('../error');
validator = require('../lib/validator'), const { PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD } = require('../permission');
db = require('../service/db'), const validator = require('../lib/validator');
coll = db.collection('contest'), const db = require('../service/db');
coll_status = db.collection('contest.status');
const coll = db.collection('contest');
const collStatus = db.collection('contest.status');
const RULES = { const RULES = {
homework: require('../module/contest/homework'), homework: require('../module/contest/homework'),
oi: require('../module/contest/oi'), oi: require('../module/contest/oi'),
acm: require('../module/contest/acm') acm: require('../module/contest/acm'),
}; };
/** /**
@@ -21,13 +23,13 @@ const RULES = {
*/ */
/** /**
* @param {string} title * @param {string} title
* @param {string} content * @param {string} content
* @param {number} owner * @param {number} owner
* @param {string} rule * @param {string} rule
* @param {Date} beginAt * @param {Date} beginAt
* @param {Date} endAt * @param {Date} endAt
* @param {ObjectID[]} pids * @param {ObjectID[]} pids
* @param {object} data * @param {object} data
* @returns {ObjectID} tid * @returns {ObjectID} tid
*/ */
@@ -37,112 +39,120 @@ async function add(title, content, owner, rule,
validator.checkContent(content); validator.checkContent(content);
if (!this.RULES[rule]) throw new ValidationError('rule'); if (!this.RULES[rule]) throw new ValidationError('rule');
if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt'); if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt');
Object.assign(data, { content, owner, title, rule, beginAt, endAt, pids, attend: 0 }); Object.assign(data, {
content, owner, title, rule, beginAt, endAt, pids, attend: 0,
});
this.RULES[rule].check(data); this.RULES[rule].check(data);
let res = await coll.insertOne(data); const res = await coll.insertOne(data);
return res.insertedId; return res.insertedId;
} }
/** /**
* @param {ObjectID} tid * @param {ObjectID} tid
* @param {object} $set * @param {object} $set
* @returns {Tdoc} tdoc after modification * @returns {Tdoc} tdoc after modification
*/ */
async function edit(tid, $set) { async function edit(tid, $set) {
if ($set.title) validator.checkTitle($set.title); if ($set.title) validator.checkTitle($set.title);
if ($set.content) validator.checkIntro($set.content); if ($set.content) validator.checkIntro($set.content);
if ($set.rule) if ($set.rule) { if (!this.RULES[$set.rule]) throw new ValidationError('rule'); }
if (!this.RULES[$set.rule]) throw new ValidationError('rule'); if ($set.beginAt && $set.endAt) if ($set.beginAt >= $set.endAt) throw new ValidationError('beginAt', 'endAt');
if ($set.beginAt && $set.endAt) const tdoc = await coll.findOne({ tid });
if ($set.beginAt >= $set.endAt) throw new ValidationError('beginAt', 'endAt');
let tdoc = await coll.findOne({ tid });
if (!tdoc) throw new ContestNotFoundError(tid); if (!tdoc) throw new ContestNotFoundError(tid);
this.RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set)); this.RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set));
await coll.findOneAndUpdate({ tid }, { $set }); await coll.findOneAndUpdate({ tid }, { $set });
return tdoc; return tdoc;
} }
/** /**
* @param {ObjectID} tid * @param {ObjectID} tid
* @returns {Tdoc} * @returns {Tdoc}
*/ */
async function get(tid) { async function get(tid) {
let tdoc = await coll.findOne({ _id: tid }); const tdoc = await coll.findOne({ _id: tid });
if (!tdoc) throw new ContestNotFoundError(tid); if (!tdoc) throw new ContestNotFoundError(tid);
return tdoc; return tdoc;
} }
async function updateStatus(tid, uid, rid, pid, accept, score) { async function updateStatus(tid, uid, rid, pid, accept, score) {
let tdoc = await get(tid); const tdoc = await get(tid);
let tsdoc = await coll_status.findOneAndUpdate({ tid: tdoc._id, uid }, { const tsdoc = await collStatus.findOneAndUpdate({ tid: tdoc._id, uid }, {
$push: { journal: { rid, pid, accept, score } }, $push: {
$inc: { rev: 1 } journal: {
rid, pid, accept, score,
},
},
$inc: { rev: 1 },
}, { upsert: true }); }, { upsert: true });
if (!tsdoc.value.attend) throw new ContestNotAttendedError(tid, uid); if (!tsdoc.value.attend) throw new ContestNotAttendedError(tid, uid);
} }
async function getListStatus(uid, tids) { async function getListStatus(uid, tids) {
let r = {}; const r = {};
for (let tid of tids) r[tid] = await this.getStatus(tid, uid); for (const tid of tids) r[tid] = await this.getStatus(tid, uid); // eslint-disable-line no-await-in-loop
return r; return r;
} }
async function attend(tid, uid) { async function attend(tid, uid) {
try { try {
await coll_status.insertOne({ tid, uid, attend: 1 }); await collStatus.insertOne({ tid, uid, attend: 1 });
} catch (e) { } catch (e) {
throw new ContestAlreadyAttendedError(tid, uid); throw new ContestAlreadyAttendedError(tid, uid);
} }
await coll.findOneAndUpdate({ _id: tid }, { $inc: { attend: 1 } }); await coll.findOneAndUpdate({ _id: tid }, { $inc: { attend: 1 } });
} }
function getMultiStatus(query) { function getMultiStatus(query) {
return coll_status.find(query); return collStatus.find(query);
} }
function is_new(tdoc, days = 1) { function is_new(tdoc, days = 1) {
let now = new Date().getTime(); const now = new Date().getTime();
let readyAt = tdoc.beginAt.getTime(); const readyAt = tdoc.beginAt.getTime();
return (now < readyAt - days * 24 * 3600 * 1000); return (now < readyAt - days * 24 * 3600 * 1000);
} }
function is_upcoming(tdoc, days = 1) { function is_upcoming(tdoc, days = 1) {
let now = new Date().getTime(); const now = new Date().getTime();
let readyAt = tdoc.beginAt.getTime(); const readyAt = tdoc.beginAt.getTime();
return (now > readyAt - days * 24 * 3600 * 1000 && now < tdoc.beginAt); return (now > readyAt - days * 24 * 3600 * 1000 && now < tdoc.beginAt);
} }
function is_not_started(tdoc) { function is_not_started(tdoc) {
return (new Date()) < tdoc.beginAt; return (new Date()) < tdoc.beginAt;
} }
function is_ongoing(tdoc) { function is_ongoing(tdoc) {
let now = new Date(); const now = new Date();
return (tdoc.beginAt <= now && now < tdoc.endAt); return (tdoc.beginAt <= now && now < tdoc.endAt);
} }
function is_done(tdoc) { function is_done(tdoc) {
return tdoc.endAt <= new Date(); return tdoc.endAt <= new Date();
} }
const ContestHandlerMixin = c => class extends c { const ContestHandlerMixin = (c) => class extends c {
canViewHiddenScoreboard() { canViewHiddenScoreboard() {
return this.user.hasPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); return this.user.hasPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
} }
canShowRecord(tdoc, allowPermOverride = true) { canShowRecord(tdoc, allowPermOverride = true) {
if (RULES[tdoc.rule].showRecord(tdoc, new Date())) return true; if (RULES[tdoc.rule].showRecord(tdoc, new Date())) return true;
if (allowPermOverride && this.canViewHiddenScoreboard(tdoc)) return true; if (allowPermOverride && this.canViewHiddenScoreboard(tdoc)) return true;
return false; return false;
} }
canShowScoreboard(tdoc, allowPermOverride = true) { canShowScoreboard(tdoc, allowPermOverride = true) {
if (RULES[tdoc.rule].showScoreboard(tdoc, new Date())) return true; if (RULES[tdoc.rule].showScoreboard(tdoc, new Date())) return true;
if (allowPermOverride && this.canViewHiddenScoreboard(tdoc)) return true; if (allowPermOverride && this.canViewHiddenScoreboard(tdoc)) return true;
return false; return false;
} }
async getScoreboard(tid, isExport = false) { async getScoreboard(tid, isExport = false) {
let tdoc = await get(tid); const tdoc = await get(tid);
if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid); if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid);
let tsdocs = await getMultiStatus(tid).sort(RULES[tdoc.rule].statusSort).toArray(); const tsdocs = await getMultiStatus(tid).sort(RULES[tdoc.rule].statusSort).toArray();
let uids = []; const uids = [];
for (let tsdoc of tsdocs) uids.push(tsdoc.uid); for (const tsdoc of tsdocs) uids.push(tsdoc.uid);
let [udict, pdict] = await Promise.all([user.getList(uids), problem.getList(tdoc['pids'])]); const [udict, pdict] = await Promise.all([user.getList(uids), problem.getList(tdoc.pids)]);
let ranked_tsdocs = RULES[tdoc.rule].rank(tsdocs); const rankedTsdocs = RULES[tdoc.rule].rank(tsdocs);
let rows = RULES[tdoc.rule].scoreboard(isExport, str => str ? str.toString().translate(this.user.language) : '', tdoc, ranked_tsdocs, udict, pdict); const rows = RULES[tdoc.rule].scoreboard(isExport, (str) => (str ? str.toString().translate(this.user.language) : ''), tdoc, rankedTsdocs, udict, pdict);
return [tdoc, rows, udict]; return [tdoc, rows, udict];
} }
async verifyProblems(pids) {
let r = []; async verifyProblems(pids) { // eslint-disable-line class-methods-use-this
for (let pid of pids) { const r = [];
let res = await problem.get(pid); for (const pid of pids) {
const res = await problem.get(pid); // eslint-disable-line no-await-in-loop
if (res) r.push(res._id); if (res) r.push(res._id);
else throw new ProblemNotFoundError(pid); else throw new ProblemNotFoundError(pid);
} }
@@ -151,26 +161,37 @@ const ContestHandlerMixin = c => class extends c {
}; };
module.exports = { module.exports = {
RULES, ContestHandlerMixin, add, getListStatus, attend, edit, get, updateStatus, RULES,
count: query => coll.find(query).count(), ContestHandlerMixin,
getMulti: query => coll.find(query), add,
getListStatus,
attend,
edit,
get,
updateStatus,
count: (query) => coll.find(query).count(),
getMulti: (query) => coll.find(query),
getStatus: (tid, uid) => coll_status.findOne({ tid, uid }), getStatus: (tid, uid) => coll_status.findOne({ tid, uid }),
setStatus: (tid, uid, $set) => coll_status.findOneAndUpdate({ tid, uid }, { $set }), setStatus: (tid, uid, $set) => coll_status.findOneAndUpdate({ tid, uid }, { $set }),
is_new, is_upcoming, is_not_started, is_ongoing, is_done, is_new,
status_text: tdoc => is_upcoming,
is_not_started,
is_ongoing,
is_done,
status_text: (tdoc) => (
is_new(tdoc) is_new(tdoc)
? 'New' ? 'New'
: is_upcoming(tdoc) : is_upcoming(tdoc)
? 'Ready (☆▽☆)' ? 'Ready (☆▽☆)'
: is_ongoing(tdoc) : is_ongoing(tdoc)
? 'Live...' ? 'Live...'
: 'Done', : 'Done'),
get_status: tdoc => get_status: (tdoc) => (
is_not_started(tdoc) is_not_started(tdoc)
? 'not_started' ? 'not_started'
: is_ongoing(tdoc) : is_ongoing(tdoc)
? 'ongoing' ? 'ongoing'
: 'finished', : 'finished'),
}; };
/* /*
@@ -336,7 +357,6 @@ def _format_pids(pids_list):
return ','.join([str(pid) for pid in pids_list]) return ','.join([str(pid) for pid in pids_list])
class ContestStatusMixin(object): class ContestStatusMixin(object):
@property @property
@functools.lru_cache() @functools.lru_cache()
@@ -447,4 +467,4 @@ class ContestCommonOperationMixin(object):
class ContestMixin(ContestStatusMixin, ContestVisibilityMixin, ContestCommonOperationMixin): class ContestMixin(ContestStatusMixin, ContestVisibilityMixin, ContestCommonOperationMixin):
pass pass
*/ */
+1 -1
View File
@@ -9,5 +9,5 @@ module.exports = {
system: require('./system'), system: require('./system'),
token: require('./token'), token: require('./token'),
training: require('./training'), training: require('./training'),
user: require('./user') user: require('./user'),
}; };
+18 -25
View File
@@ -1,35 +1,28 @@
const const { OpcountExceededError } = require('../error');
{ OpcountExceededError } = require('../error'), const db = require('../service/db.js');
db = require('../service/db.js'),
coll = db.collection('opcount'); const coll = db.collection('opcount');
module.exports = { module.exports = {
/** /**
* @param {string} op * @param {string} op
* @param {string} ident * @param {string} ident
* @param {number} period_secs * @param {number} period_secs
* @param {number} max_operations * @param {number} max_operations
*/ */
async inc(op, ident, period_secs, max_operations) { async inc(op, ident, periodSecs, maxOperations) {
let cur_time = new Date().getTime(); const curTime = new Date().getTime();
let begin_at = new Date(cur_time - cur_time % (period_secs * 1000)); const beginAt = new Date(curTime - (curTime % (periodSecs * 1000)));
let expire_at = new Date(begin_at.getTime() + period_secs * 1000); const expireAt = new Date(beginAt.getTime() + periodSecs * 1000);
try { try {
await coll.findOneAndUpdate({ await coll.findOneAndUpdate({
ident, begin_at, expire_at, ident,
op: { $not: { $gte: max_operations } } beginAt,
expireAt,
op: { $not: { $gte: maxOperations } },
}, { $inc: { op: 1 } }, { upsert: true }); }, { $inc: { op: 1 } }, { upsert: true });
} catch (e) { } catch (e) {
throw new OpcountExceededError(op, period_secs, max_operations); throw new OpcountExceededError(op, periodSecs, maxOperations);
} }
} },
}; };
/*
@argmethod.wrap
async def ensure_indexes():
coll = db.coll('opcount')
await coll.create_index([('ident', 1),
('begin_at', 1),
('expire_at', 1)], unique=True)
await coll.create_index('expire_at', expireAfterSeconds=0)
*/
+34 -37
View File
@@ -1,10 +1,10 @@
const const { ObjectID } = require('bson');
{ ObjectID } = require('bson'), const { ProblemNotFoundError } = require('../error');
{ ProblemNotFoundError } = require('../error'), const validator = require('../lib/validator');
validator = require('../lib/validator'), const db = require('../service/db.js');
db = require('../service/db.js'),
coll = db.collection('problem'), const coll = db.collection('problem');
coll_status = db.collection('problem.status'); const collStatus = db.collection('problem.status');
/** /**
* @typedef {import('../interface').Pdoc} Pdoc * @typedef {import('../interface').Pdoc} Pdoc
@@ -13,14 +13,14 @@ const
*/ */
/** /**
* @param {string} title * @param {string} title
* @param {string} content * @param {string} content
* @param {number} owner * @param {number} owner
* @param {number} pid * @param {number} pid
* @param {import('bson').ObjectID} data * @param {import('bson').ObjectID} data
* @param {string[]} category * @param {string[]} category
* @param {string[]} tag * @param {string[]} tag
* @param {boolean} hidden * @param {boolean} hidden
*/ */
async function add({ async function add({
title, title,
@@ -30,7 +30,7 @@ async function add({
data = null, data = null,
category = [], category = [],
tag = [], tag = [],
hidden = false hidden = false,
}) { }) {
validator.checkTitle(title); validator.checkTitle(title);
validator.checkContent(content); validator.checkContent(content);
@@ -44,7 +44,7 @@ async function add({
tag, tag,
hidden, hidden,
nSubmit: 0, nSubmit: 0,
nAccept: 0 nAccept: 0,
}); });
return pid; return pid;
} }
@@ -55,13 +55,13 @@ async function add({
*/ */
async function get(pid, uid = null) { async function get(pid, uid = null) {
let query = {}; let query = {};
if (pid.generationTime || pid.length == 24) query = { _id: new ObjectID(pid) }; if (pid.generationTime || pid.length === 24) query = { _id: new ObjectID(pid) };
else query = { pid: parseInt(pid) || pid }; else query = { pid: parseInt(pid) || pid };
let pdoc = await coll.findOne(query); const pdoc = await coll.findOne(query);
if (!pdoc) throw new ProblemNotFoundError(pid); if (!pdoc) throw new ProblemNotFoundError(pid);
if (uid) { if (uid) {
query.uid = uid; query.uid = uid;
pdoc.psdoc = await coll_status.findOne(query); pdoc.psdoc = await collStatus.findOne(query);
} }
return pdoc; return pdoc;
} }
@@ -71,7 +71,7 @@ async function get(pid, uid = null) {
*/ */
async function getById(_id) { async function getById(_id) {
_id = new ObjectID(_id); _id = new ObjectID(_id);
let pdoc = await coll.findOne({ _id }); const pdoc = await coll.findOne({ _id });
if (!pdoc) throw new ProblemNotFoundError(_id); if (!pdoc) throw new ProblemNotFoundError(_id);
return pdoc; return pdoc;
} }
@@ -81,7 +81,8 @@ async function getById(_id) {
* @returns {Pdoc[]} * @returns {Pdoc[]}
*/ */
function getMany(query, sort, page, limit) { function getMany(query, sort, page, limit) {
return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit).toArray(); return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit)
.toArray();
} }
/** /**
* @param {object} query * @param {object} query
@@ -99,28 +100,24 @@ async function edit(_id, $set) {
if ($set.title) validator.checkTitle($set.title); if ($set.title) validator.checkTitle($set.title);
if ($set.content) validator.checkContent($set.content); if ($set.content) validator.checkContent($set.content);
await coll.findOneAndUpdate({ _id }, { $set }); await coll.findOneAndUpdate({ _id }, { $set });
let pdoc = await getById(_id); const pdoc = await getById(_id);
if (!pdoc) throw new ProblemNotFoundError(_id); if (!pdoc) throw new ProblemNotFoundError(_id);
return pdoc; return pdoc;
} }
/** function count(query) {
* @param {object} query return coll.find(query).count();
* @returns {number}
*/
async function count(query) {
return await coll.find(query).count();
} }
async function random(query) { async function random(query) {
let pdocs = coll.find(query); const pdocs = coll.find(query);
let pcount = await pdocs.count(); const pcount = await pdocs.count();
if (pcount) { if (pcount) {
let pdoc = await pdocs.skip(Math.floor(Math.random() * pcount)).limit(1).toArray()[0]; const pdoc = await pdocs.skip(Math.floor(Math.random() * pcount)).limit(1).toArray()[0];
return pdoc.pid; return pdoc.pid;
} else return null; } return null;
} }
async function getList(pids) { async function getList(pids) {
let r = {}; const r = {};
for (let pid of pids) r[pid] = await get(pid); for (const pid of pids) r[pid] = await get(pid); // eslint-disable-line no-await-in-loop
return r; return r;
} }
@@ -133,5 +130,5 @@ module.exports = {
random, random,
getById, getById,
getMulti, getMulti,
getList getList,
}; };
+27 -26
View File
@@ -1,13 +1,13 @@
const const _ = require('lodash');
_ = require('lodash'), const { ObjectID } = require('bson');
{ ObjectID } = require('bson'), const { RecordNotFoundError } = require('../error');
{ RecordNotFoundError } = require('../error'), const { STATUS_WAITING } = require('./builtin').STATUS;
{ STATUS_WAITING } = require('../model/builtin').STATUS, const db = require('../service/db.js');
db = require('../service/db.js'),
coll = db.collection('record'); const coll = db.collection('record');
/** /**
* @param {import('../interface').Record} data * @param {import('../interface').Record} data
*/ */
async function add(data) { async function add(data) {
_.defaults(data, { _.defaults(data, {
@@ -20,33 +20,34 @@ async function add(data) {
compilerTexts: [], compilerTexts: [],
testCases: [], testCases: [],
judger: null, judger: null,
judgeAt: null judgeAt: null,
}); });
let res = await coll.insertOne(data); const res = await coll.insertOne(data);
return res.insertedId; return res.insertedId;
} }
/** /**
* @param {string} rid * @param {string} rid
* @returns {import('../interface').Record} * @returns {import('../interface').Record}
*/ */
async function get(rid) { async function get(rid) {
let _id = new ObjectID(rid); const _id = new ObjectID(rid);
let rdoc = await coll.findOne({ _id }); const rdoc = await coll.findOne({ _id });
if (!rdoc) throw new RecordNotFoundError(rid); if (!rdoc) throw new RecordNotFoundError(rid);
return rdoc; return rdoc;
} }
async function getMany(query, sort, page, limit) { function getMany(query, sort, page, limit) {
return await coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit).toArray(); return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit)
.toArray();
} }
async function update(rid, $set) { async function update(rid, $set) {
let _id = new ObjectID(rid); const _id = new ObjectID(rid);
await coll.findOneAndUpdate({ _id }, { $set }); await coll.findOneAndUpdate({ _id }, { $set });
let rdoc = await coll.findOne({ _id }); const rdoc = await coll.findOne({ _id });
if (!rdoc) throw new RecordNotFoundError(rid); if (!rdoc) throw new RecordNotFoundError(rid);
return rdoc; return rdoc;
} }
async function reset(rid) { function reset(rid) {
return await update(rid, { return update(rid, {
score: 0, score: 0,
status: STATUS_WAITING, status: STATUS_WAITING,
time: 0, time: 0,
@@ -56,15 +57,15 @@ async function reset(rid) {
compilerTexts: [], compilerTexts: [],
judgeAt: null, judgeAt: null,
judger: null, judger: null,
rejudged: true rejudged: true,
}); });
} }
async function count(query) { function count(query) {
return await coll.find(query).count(); return coll.find(query).count();
} }
async function getList(rids) { async function getList(rids) {
let r = {}; const r = {};
for (let rid of rids) r[rid] = await get(rid); for (const rid of rids) r[rid] = await get(rid); // eslint-disable-line no-await-in-loop
return r; return r;
} }
function getUserInProblemMulti(uid, pid) { function getUserInProblemMulti(uid, pid) {
@@ -79,5 +80,5 @@ module.exports = {
count, count,
reset, reset,
getList, getList,
getUserInProblemMulti getUserInProblemMulti,
}; };
+46 -45
View File
@@ -1,93 +1,94 @@
const const { ObjectID } = require('bson');
{ ObjectID } = require('bson'), const { SolutionNotFoundError } = require('../error');
{ SolutionNotFoundError, AlreadyVotedError } = require('../error'), const validator = require('../lib/validator');
validator = require('../lib/validator'), const db = require('../service/db.js');
db = require('../service/db.js'),
coll = db.collection('solution'),
coll_status = db.collection('solution.status');
/** const coll = db.collection('solution');
const collStatus = db.collection('solution.status');
/**
* @param {string} pid * @param {string} pid
* @param {number} owner * @param {number} owner
* @param {string} content * @param {string} content
*/ */
async function add(pid, owner, content) { async function add(pid, owner, content) {
validator.checkContent(content); validator.checkContent(content);
pid = new ObjectID(pid); pid = new ObjectID(pid);
let res = await coll.insertOne({ content, owner, pid, reply: [], vote: 0 }); const res = await coll.insertOne({
content, owner, pid, reply: [], vote: 0,
});
return res.insertedId; return res.insertedId;
} }
async function get(psid) { async function get(psid) {
psid = new ObjectID(psid); psid = new ObjectID(psid);
let psdoc = await coll.findOne({ _id: psid }); const psdoc = await coll.findOne({ _id: psid });
if (!psdoc) throw new SolutionNotFoundError(); if (!psdoc) throw new SolutionNotFoundError();
return psdoc; return psdoc;
} }
async function getMany(query, sort, page, limit) { function getMany(query, sort, page, limit) {
return await coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit).toArray(); return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit)
.toArray();
} }
async function edit(_id, content) { async function edit(_id, content) {
validator.checkContent(content); validator.checkContent(content);
await coll.findOneAndUpdate({ _id }, { $set: { content } }); await coll.findOneAndUpdate({ _id }, { $set: { content } });
let psdoc = await get(_id); const psdoc = await get(_id);
if (!psdoc) throw new SolutionNotFoundError(_id); if (!psdoc) throw new SolutionNotFoundError(_id);
return psdoc; return psdoc;
} }
async function del(psid) { function del(psid) {
psid = new ObjectID(psid); return coll.deleteOne({ _id: psid });
return await coll.deleteOne({ _id: psid });
} }
async function count(query) { function count(query) {
return await coll.find(query).count(); return coll.find(query).count();
} }
function getMulti(pid) { function getMulti(pid) {
return coll.find({ pid }).sort({ vote: -1 }); return coll.find({ pid }).sort({ vote: -1 });
} }
async function reply(psid, owner, content) { function reply(psid, owner, content) {
psid = new ObjectID(psid);
validator.checkContent(content); validator.checkContent(content);
return await coll.findOneAndUpdate({ _id: psid }, { $push: { reply: { content, owner, _id: new ObjectID() } } }); return coll.findOneAndUpdate(
{ _id: psid },
{ $push: { reply: { content, owner, _id: new ObjectID() } } },
);
} }
async function getReply(psid, psrid) { async function getReply(psid, psrid) {
psid = new ObjectID(psid); const psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } });
psrid = new ObjectID(psrid);
let psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } });
if (!psdoc) return [null, null]; if (!psdoc) return [null, null];
for (let psrdoc of psdoc) for (const psrdoc of psdoc) if (psrdoc._id === psrid) return [psdoc, psrdoc];
if (psrdoc._id == psrid)
return [psdoc, psrdoc];
return [psdoc, null]; return [psdoc, null];
} }
async function editReply(psid, psrid, content) { async function editReply(psid, psrid, content) {
validator.checkContent(content); validator.checkContent(content);
psid = new ObjectID(psid); psid = new ObjectID(psid);
psrid = new ObjectID(psrid); psrid = new ObjectID(psrid);
let psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } }); const psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } });
let reply = psdoc.reply; const { reply } = psdoc;
for (let i in reply) for (const i in reply) {
if (reply[i]._id == psrid) { if (reply[i]._id === psrid) {
reply[i].content = content; reply[i].content = content;
break; break;
} }
}
return await coll.findOneAndUpdate({ _id: psdoc._id }, { $set: { reply } }); return await coll.findOneAndUpdate({ _id: psdoc._id }, { $set: { reply } });
} }
async function delReply(psid, psrid) { function delReply(psid, psrid) {
return await coll.findOneAndUpdate({ _id: psid }, { $pull: { reply: { _id: psrid } } }); return coll.findOneAndUpdate({ _id: psid }, { $pull: { reply: { _id: psrid } } });
} }
async function vote(psid, uid, value) { async function vote(psid, uid, value) {
let pssdoc = await coll_status.findOne({ psid, uid }); let pssdoc = await collStatus.findOne({ psid, uid });
if (pssdoc) await coll_status.deleteOne({ psid, uid }); if (pssdoc) await collStatus.deleteOne({ psid, uid });
await coll_status.insertOne({ psid, uid, vote: value }); await collStatus.insertOne({ psid, uid, vote: value });
if (pssdoc) value += -pssdoc.vote; if (pssdoc) value += -pssdoc.vote;
await coll.findOneAndUpdate({ _id: psid }, { $inc: { vote: value } }); await coll.findOneAndUpdate({ _id: psid }, { $inc: { vote: value } });
pssdoc = await coll_status.findOne({ psid, uid }); pssdoc = await collStatus.findOne({ psid, uid });
let psdoc = await coll.findOne({ _id: psid }); const psdoc = await coll.findOne({ _id: psid });
return [psdoc, pssdoc]; return [psdoc, pssdoc];
} }
async function getListStatus(list, uid) { async function getListStatus(list, uid) {
let result = {}; const result = {};
let res = await coll_status.find({ uid, psid: { $in: list } }).toArray(); const res = await collStatus.find({ uid, psid: { $in: list } }).toArray();
for (let i of res) result[i.psid] = i; for (const i of res) result[i.psid] = i;
return result; return result;
} }
module.exports = { module.exports = {
@@ -103,5 +104,5 @@ module.exports = {
editReply, editReply,
delReply, delReply,
vote, vote,
getListStatus getListStatus,
}; };
+15 -82
View File
@@ -1,42 +1,43 @@
const const db = require('../service/db.js');
db = require('../service/db.js'),
coll = db.collection('system'); const coll = db.collection('system');
async function get(_id) { async function get(_id) {
let doc = await coll.findOne({ _id }); const doc = await coll.findOne({ _id });
if (doc) return doc.value; if (doc) return doc.value;
return null;
} }
async function update(_id, update, config) { async function update(_id, update, config) {
await coll.findOneAndUpdate({ _id }, update, config); await coll.findOneAndUpdate({ _id }, update, config);
return this.get(_id); return get(_id);
} }
/** /**
* Increments the user counter. * Increments the user counter.
* @returns {number} Integer value after increment. * @returns {number} Integer value after increment.
*/ */
async function incUserCounter() { function incUserCounter() {
return await this.update('userCounter', { $inc: { value: 1 } }, { upsert: true }); return update('userCounter', { $inc: { value: 1 } }, { upsert: true });
} }
/** /**
* Increments the problem ID counter. * Increments the problem ID counter.
* @returns {number} Integer value before increment. * @returns {number} Integer value before increment.
*/ */
async function incPidCounter() { function incPidCounter() {
return await this.update('userCounter', { $inc: { value: 1 } }, { upsert: true }); return update('userCounter', { $inc: { value: 1 } }, { upsert: true });
} }
async function acquireLock(name) { async function acquireLock(name) {
let value = Math.floor(Math.random() * 0xFFFFFFFF); const value = Math.floor(Math.random() * 0xFFFFFFFF);
try { try {
await coll.updateOne({ _id: 'lock_' + name, value: 0 }, { $set: { value } }, { upsert: true }); await coll.updateOne({ _id: `lock_${name}`, value: 0 }, { $set: { value } }, { upsert: true });
} catch (e) { return null; } } catch (e) { return null; }
return value; return value;
} }
async function releaseLock(name, value) { async function releaseLock(name, value) {
let result = await coll.updateOne({ _id: 'lock_' + name, value }, { $set: { value: 0 } }); const result = await coll.updateOne({ _id: `lock_${name}`, value }, { $set: { value: 0 } });
return !!result.matchedCount; return !!result.matchedCount;
} }
async function releaseLockAnyway(name) { async function releaseLockAnyway(name) {
await coll.updateOne({ _id: 'lock_' + name }, { $set: { value: 0 } }); await coll.updateOne({ _id: `lock_${name}` }, { $set: { value: 0 } });
return true; return true;
} }
module.exports = { module.exports = {
@@ -46,73 +47,5 @@ module.exports = {
incUserCounter, incUserCounter,
acquireLock, acquireLock,
releaseLock, releaseLock,
releaseLockAnyway releaseLockAnyway,
}; };
/*
EXPECTED_DB_VERSION = 1
async def acquire_upgrade_lock():
lock = await acquire_lock('upgrade')
if not lock:
raise error.UpgradeLockAcquireError()
return lock
async def release_upgrade_lock(lock: int):
success = await release_lock('upgrade', lock)
if not success:
raise error.UpgradeLockReleaseError()
return True
@argmethod.wrap
async def release_upgrade_lock_anyway():
return await release_lock_anyway('upgrade')
@argmethod.wrap
async def get_db_version():
coll = db.coll('system')
doc = await coll.find_one({'_id': 'db_version'})
if doc is None:
return 0
else:
return doc['value']
async def set_db_version(version: int):
coll = db.coll('system')
result = await coll.update_one(filter={'_id': 'db_version'},
update={'$set': {'value': version}},
upsert=True)
return result.modified_count
async def ensure_db_version(allowed_version=None):
if allowed_version is None:
allowed_version = EXPECTED_DB_VERSION
current_version = await get_db_version()
if current_version != allowed_version:
raise error.DatabaseVersionMismatchError(current_version, allowed_version)
@argmethod.wrap
async def setup():
"""
Set up for fresh install
"""
coll = db.coll('system')
fdoc = await coll.find_one({'_id': 'user_counter'})
if fdoc:
# skip if not fresh install
return
await set_db_version(EXPECTED_DB_VERSION)
if __name__ == '__main__':
argmethod.invoke_by_args()
*/
+20 -17
View File
@@ -1,6 +1,6 @@
const const db = require('../service/db.js');
db = require('../service/db.js'),
coll = db.collection('token'); const coll = db.collection('token');
module.exports = { module.exports = {
TYPE_SESSION: 0, TYPE_SESSION: 0,
@@ -14,15 +14,16 @@ module.exports = {
* @returns {Array} token ID, token data * @returns {Array} token ID, token data
*/ */
async add(tokenType, expireSeconds, data) { async add(tokenType, expireSeconds, data) {
let now = new Date(); const now = new Date();
let str = String.random(32); const str = String.random(32);
let res = await coll.insertOne(Object.assign({}, data, { const res = await coll.insertOne({
...data,
_id: str, _id: str,
tokenType, tokenType,
createAt: now, createAt: now,
updateAt: now, updateAt: now,
expireAt: new Date(now.getTime() + expireSeconds * 1000) expireAt: new Date(now.getTime() + expireSeconds * 1000),
})); });
return [str, res.ops]; return [str, res.ops];
}, },
@@ -45,12 +46,14 @@ module.exports = {
* @returns {object} The token document, or null. * @returns {object} The token document, or null.
*/ */
async update(tokenId, tokenType, expireSeconds, data) { async update(tokenId, tokenType, expireSeconds, data) {
let now = new Date(); const now = new Date();
let res = await coll.findOneAndUpdate({ _id: tokenId, tokenType }, { const res = await coll.findOneAndUpdate({ _id: tokenId, tokenType }, {
$set: Object.assign({ $set: {
updateAt: now, updateAt: now,
expireAt: new Date(now.getTime() + expireSeconds * 1000) expireAt: new Date(now.getTime() + expireSeconds * 1000),
}, data, { tokenType }) ...data,
tokenType,
},
}); });
return res.value; return res.value;
}, },
@@ -62,10 +65,10 @@ module.exports = {
* @returns {boolean} true if deleted, or false. * @returns {boolean} true if deleted, or false.
*/ */
async delete(tokenId, tokenType) { async delete(tokenId, tokenType) {
let result = await coll.deleteOne({ _id: tokenId, tokenType }); const result = await coll.deleteOne({ _id: tokenId, tokenType });
return !!result.deletedCount; return !!result.deletedCount;
}, },
async getMostRecentSessionByUid(uid) { getMostRecentSessionByUid(uid) {
return await coll.findOne({ uid, token_type: this.TYPE_SESSION }, { sort: { updateAt: -1 } }); return coll.findOne({ uid, token_type: this.TYPE_SESSION }, { sort: { updateAt: -1 } });
} },
}; };
+25 -27
View File
@@ -1,26 +1,27 @@
const const validator = require('../lib/validator');
validator = require('../lib/validator'), const { ValidationError, TrainingNotFoundError } = require('../error');
{ ValidationError, TrainingNotFoundError } = require('../error'), const db = require('../service/db.js');
db = require('../service/db.js'),
coll = db.collection('traning'), const coll = db.collection('traning');
coll_status = db.collection('training.status'); const collStatus = db.collection('training.status');
module.exports = { module.exports = {
SETTING_DIFFICULTY_ALGORITHM: 0, SETTING_DIFFICULTY_ALGORITHM: 0,
SETTING_DIFFICULTY_ADMIN: 1, SETTING_DIFFICULTY_ADMIN: 1,
SETTING_DIFFICULTY_AVERAGE: 2, SETTING_DIFFICULTY_AVERAGE: 2,
async add(title, content, owner_uid, dag = [], desc = '') { async add(title, content, owner, dag = [], desc = '') {
validator.check_title(title); validator.checkTitle(title);
validator.check_intro(content); validator.checkIntro(content);
validator.check_description(desc); validator.checkDescription(desc);
for (let node of dag) for (const node of dag) { for (const nid of node.require_nids) if (nid >= node._id) throw new ValidationError('dag'); }
for (let nid of node['require_nids'])
if (nid >= node._id)
throw new ValidationError('dag');
return await coll.insertOne({ return await coll.insertOne({
content, owner_uid, content,
dag, title, desc, enroll: 0 owner,
dag,
title,
desc,
enroll: 0,
}); });
}, },
count: (query) => coll.find(query).count(), count: (query) => coll.find(query).count(),
@@ -28,24 +29,21 @@ module.exports = {
if ($set.title) validator.check_title($set.title); if ($set.title) validator.check_title($set.title);
if ($set.content) validator.check_intro($set.content); if ($set.content) validator.check_intro($set.content);
if ($set.desc) validator.check_description($set.desc); if ($set.desc) validator.check_description($set.desc);
if ($set.dag) if ($set.dag) { for (const node of $set.dag) for (const nid of node.require_nids) if (nid >= node._id) throw new ValidationError('dag'); }
for (let node of $set.dag)
for (let nid of node.require_nids)
if (nid >= node._id) throw new ValidationError('dag');
await coll.findOneAndUpdate({ tid }, { $set }); await coll.findOneAndUpdate({ tid }, { $set });
let tdoc = await coll.findOne({ tid }); const tdoc = await coll.findOne({ tid });
if (!tdoc) throw new TrainingNotFoundError(tid); if (!tdoc) throw new TrainingNotFoundError(tid);
return tdoc; return tdoc;
}, },
async get(tid) { async get(tid) {
let tdoc = await coll.findOne({ tid }); const tdoc = await coll.findOne({ tid });
if (!tdoc) throw new TrainingNotFoundError(tid); if (!tdoc) throw new TrainingNotFoundError(tid);
return tdoc; return tdoc;
}, },
get_multi: (query) => coll.find( query), get_multi: (query) => coll.find(query),
get_multi_status: (query) => coll_status.find(query), get_multi_status: (query) => collStatus.find(query),
get_status: (tid, uid) => coll_status.findOne({ tid, uid }), get_status: (tid, uid) => collStatus.findOne({ tid, uid }),
set_status: (tid, uid, $set) => coll_status.findOneAndUpdate({ tid, uid }, { $set }) set_status: (tid, uid, $set) => collStatus.findOneAndUpdate({ tid, uid }, { $set }),
}; };
@@ -66,4 +64,4 @@ async def get_dict(domainId, tids, *, fields=None):
fields=fields): fields=fields):
result[tdoc['doc_id']] = tdoc result[tdoc['doc_id']] = tdoc
return result return result
*/ */
+51 -42
View File
@@ -1,11 +1,11 @@
const const system = require('./system');
system = require('./system'), const { UserNotFoundError, UserAlreadyExistError } = require('../error');
{ UserNotFoundError, UserAlreadyExistError } = require('../error'), const pwhash = require('../lib/pwhash');
pwhash = require('../lib/pwhash'), const validator = require('../lib/validator');
validator = require('../lib/validator'), const db = require('../service/db');
db = require('../service/db'),
coll = db.collection('user'), const coll = db.collection('user');
coll_role = db.collection('role'); const collRole = db.collection('role');
class USER { class USER {
constructor(user) { constructor(user) {
@@ -24,75 +24,78 @@ class USER {
this.nAccept = user.nAccept || 0; this.nAccept = user.nAccept || 0;
this.nSubmit = user.nSubmit || 0; this.nSubmit = user.nSubmit || 0;
} }
hasPerm(perm) { hasPerm(perm) {
return this.perm == '-' || (this.perm || '').includes(perm); return this.perm === '-' || (this.perm || '').includes(perm);
} }
checkPassword(password) { checkPassword(password) {
return pwhash.check(password, this.salt, this.hash); return pwhash.check(password, this.salt, this.hash);
} }
} }
async function getById(_id) { async function getById(_id) {
let udoc = await coll.findOne({ _id }); const udoc = await coll.findOne({ _id });
if (!udoc) throw new UserNotFoundError(_id); if (!udoc) throw new UserNotFoundError(_id);
let role = await coll_role.findOne({ _id: udoc.role || 'default' }); const role = await collRole.findOne({ _id: udoc.role || 'default' });
udoc.perm = role.perm; udoc.perm = role.perm;
return new USER(udoc); return new USER(udoc);
} }
async function getList(uids) { async function getList(uids) {
let r = {}; const r = {};
for (let uid of uids) for (const uid of uids) r[uid] = await getById(uid); // eslint-disable-line no-await-in-loop
r[uid] = await getById(uid);
return r; return r;
} }
async function getByUname(uname) { async function getByUname(uname) {
let unameLower = uname.trim().toLowerCase(); const unameLower = uname.trim().toLowerCase();
let udoc = await coll.findOne({ unameLower }); const udoc = await coll.findOne({ unameLower });
if (!udoc) throw new UserNotFoundError(uname); if (!udoc) throw new UserNotFoundError(uname);
let role = await coll_role.findOne({ _id: udoc.role || 'default' }); const role = await collRole.findOne({ _id: udoc.role || 'default' });
udoc.perm = role.perm; udoc.perm = role.perm;
return new USER(udoc); return new USER(udoc);
} }
async function getByEmail(mail, ignoreMissing = false) { async function getByEmail(mail, ignoreMissing = false) {
let mailLower = mail.trim().toLowerCase(); const mailLower = mail.trim().toLowerCase();
let udoc = await coll.findOne({ mailLower }); const udoc = await coll.findOne({ mailLower });
if (!udoc) { if (!udoc) {
if (ignoreMissing) return null; if (ignoreMissing) return null;
else throw new UserNotFoundError(mail); throw new UserNotFoundError(mail);
} }
let role = await coll_role.findOne({ _id: udoc.role || 'default' }); const role = await collRole.findOne({ _id: udoc.role || 'default' });
udoc.perm = role.perm; udoc.perm = role.perm;
return new USER(udoc); return new USER(udoc);
} }
async function setPassword(uid, password) { function setPassword(uid, password) {
validator.checkPassword(password); validator.checkPassword(password);
let salt = pwhash.salt(); const salt = pwhash.salt();
return await coll.findOneAndUpdate({ _id: uid }, { return coll.findOneAndUpdate({ _id: uid }, {
$set: { salt, hash: pwhash.hash(password, salt) } $set: { salt, hash: pwhash.hash(password, salt) },
}); });
} }
async function setEmail(uid, mail) {
validator.checkEmail(mail);
return await setById(uid, { mail, mailLower: mail.trim().toLowerCase() });
}
function setById(uid, args) { function setById(uid, args) {
coll.findOneAndUpdate({ _id: uid }, { $set: args }); coll.findOneAndUpdate({ _id: uid }, { $set: args });
} }
function setEmail(uid, mail) {
validator.checkEmail(mail);
return setById(uid, { mail, mailLower: mail.trim().toLowerCase() });
}
async function changePassword(uid, currentPassword, newPassword) { async function changePassword(uid, currentPassword, newPassword) {
validator.checkPassword(newPassword); validator.checkPassword(newPassword);
let udoc = await getById(uid); const udoc = await getById(uid);
udoc.checkPassword(currentPassword); udoc.checkPassword(currentPassword);
let salt = pwhash.salt(); const salt = pwhash.salt();
return await coll.findOneAndUpdate({ return await coll.findOneAndUpdate({
_id: udoc._id _id: udoc._id,
}, { }, {
$set: { salt, hash: pwhash.hash(newPassword, salt) } $set: { salt, hash: pwhash.hash(newPassword, salt) },
}); });
} }
async function create({ uid, mail, uname, password, regip = '127.0.0.1', role = 'default' }) { async function create({
uid, mail, uname, password, regip = '127.0.0.1', role = 'default',
}) {
validator.checkUname(uname); validator.checkUname(uname);
validator.checkPassword(password); validator.checkPassword(password);
validator.checkEmail(mail); validator.checkEmail(mail);
let salt = pwhash.salt(); const salt = pwhash.salt();
if (!uid) uid = system.incUserCounter(); if (!uid) uid = system.incUserCounter();
try { try {
await coll.insertOne({ await coll.insertOne({
@@ -108,10 +111,10 @@ async function create({ uid, mail, uname, password, regip = '127.0.0.1', role =
loginat: new Date(), loginat: new Date(),
loginip: regip, loginip: regip,
role, role,
gravatar: mail gravatar: mail,
}); });
} catch (e) { } catch (e) {
throw new UserAlreadyExistError([uid, uname, email]); throw new UserAlreadyExistError([uid, uname, mail]);
} }
} }
@@ -120,8 +123,14 @@ function getMany(params) {
} }
module.exports = { module.exports = {
changePassword, create, getByEmail, changePassword,
getById, getByUname, getMany, create,
setById, setEmail, getByEmail,
setPassword, getList getById,
}; getByUname,
getMany,
setById,
setEmail,
setPassword,
getList,
};
+37 -38
View File
@@ -4,77 +4,76 @@ module.exports = {
showScoreboard: () => true, showScoreboard: () => true,
showRecord: (tdoc, now) => now > tdoc.endAt, showRecord: (tdoc, now) => now > tdoc.endAt,
stat: (tdoc, journal) => { stat: (tdoc, journal) => {
let naccept = {}, effective = {}, detail = [], accept = 0, time = 0; const naccept = {}; const effective = {}; const detail = []; let accept = 0; let time = 0;
for (let j in journal) { for (const j in journal) {
if (tdoc.pids.includes(j.pid) && !(effective.includes(j.pid) && effective[j.pid].accept)) if (tdoc.pids.includes(j.pid) && !(effective.includes(j.pid) && effective[j.pid].accept)) { effective[j.pid] = j; }
effective[j.pid] = j; if (!j.accept) { naccept[j.pid]++; }
if (!j.accept)
naccept[j.pid]++;
} }
function _time(jdoc) { function _time(jdoc) {
let real = jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000); const real = jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000);
let penalty = 20 * 60 * naccept[jdoc.pid]; const penalty = 20 * 60 * naccept[jdoc.pid];
return real + penalty; return real + penalty;
} }
for (let j of effective) for (const j of effective) { detail.push({ ...j, naccept: naccept[j.pid], time: _time(j) }); }
detail.push(Object.assign({}, j, { for (const d of detail) {
naccept: naccept[j.pid], time: _time(j)
}));
for (let d of detail) {
accept += d.accept; accept += d.accept;
if (d.accept) time += d.time; if (d.accept) time += d.time;
} }
return { accept, time, detail }; return { accept, time, detail };
}, },
scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, dudict, pdict) { scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, dudict, pdict) {
let columns = [ const columns = [
{ type: 'rank', value: _('Rank') }, { type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') }, { type: 'user', value: _('User') },
{ type: 'display_name', value: _('Display Name') }, { type: 'display_name', value: _('Display Name') },
{ type: 'solved_problems', value: _('Solved Problems') } { type: 'solved_problems', value: _('Solved Problems') },
]; ];
if (is_export) { if (is_export) {
columns.push({ 'type': 'total_time', 'value': _('Total Time (Seconds)') }); columns.push({ type: 'total_time', value: _('Total Time (Seconds)') });
columns.push({ 'type': 'total_time_str', 'value': _('Total Time') }); columns.push({ type: 'total_time_str', value: _('Total Time') });
} }
for (let i in tdoc.pids) for (const i in tdoc.pids) {
if (is_export) { if (is_export) {
columns.push({ columns.push({
type: 'problem_flag', type: 'problem_flag',
value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title) value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title),
}); });
columns.push({ columns.push({
type: 'problem_time', type: 'problem_time',
value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')) value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')),
}); });
columns.push({ columns.push({
type: 'problem_time_str', type: 'problem_time_str',
value: '#{0} {1}'.format(i + 1, _('Time')) value: '#{0} {1}'.format(i + 1, _('Time')),
}); });
} else } else {
columns.push({ columns.push({
type: 'problem_detail', type: 'problem_detail',
value: '#{0}'.format(i + 1), raw: pdict[tdoc.pids[i]] value: '#{0}'.format(i + 1),
raw: pdict[tdoc.pids[i]],
}); });
let rows = [columns]; }
for (let [rank, tsdoc] of ranked_tsdocs) { }
let tsddict = {}; const rows = [columns];
if (tdoc.detail) for (const [rank, tsdoc] of ranked_tsdocs) {
for (let item of tsdoc.detail) const tsddict = {};
tsddict[item.pid] = item; if (tdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; }
let row = []; const row = [];
row.push( row.push(
{ type: 'string', value: rank }, { type: 'string', value: rank },
{ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }, { type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] },
{ type: 'display_name', value: (dudict[tsdoc.uid] || {}).display_name || '' }, { type: 'display_name', value: (dudict[tsdoc.uid] || {}).display_name || '' },
{ type: 'string', value: tsdoc.accept || 0 }); { type: 'string', value: tsdoc.accept || 0 },
if (is_export) );
if (is_export) {
row.push( row.push(
{ type: 'string', value: tsdoc.time || 0.0 }, { type: 'string', value: tsdoc.time || 0.0 },
{ type: 'string', value: tsdoc.time || 0.0 } { type: 'string', value: tsdoc.time || 0.0 },
); );
for (let pid of tdoc.pids) { }
let rdoc, col_accepted, col_time, col_time_str; for (const pid of tdoc.pids) {
let rdoc; let col_accepted; let col_time; let
col_time_str;
if ((tsddict[pid] || {}).accept) { if ((tsddict[pid] || {}).accept) {
rdoc = tsddict[pid].rid; rdoc = tsddict[pid].rid;
col_accepted = _('Accepted'); col_accepted = _('Accepted');
@@ -94,12 +93,12 @@ module.exports = {
row.push({ row.push({
type: 'record', type: 'record',
value: '{0}\n{1}'.format(col_accepted, col_time_str), value: '{0}\n{1}'.format(col_accepted, col_time_str),
raw: rdoc raw: rdoc,
}); });
} }
rows.push(row); rows.push(row);
} }
} }
return rows; return rows;
} },
}; };
+60 -53
View File
@@ -1,4 +1,5 @@
const { ValidationError } = require('../../error'); const { ValidationError } = require('../../error');
module.exports = { module.exports = {
TEXT: 'Assignment', TEXT: 'Assignment',
check(data) { check(data) {
@@ -7,88 +8,94 @@ module.exports = {
if (data.penalty_since > data.end_at) throw new ValidationError('penalty_since', 'end_at'); if (data.penalty_since > data.end_at) throw new ValidationError('penalty_since', 'end_at');
}, },
stat(tdoc, journal) { stat(tdoc, journal) {
let effective = {}, detail = [], score = 0, penalty_score = 0, time = 0; const effective = {};
for (let j in journal) const detail = [];
if (tdoc.pids.includes(j.pid) && !(effective.includes(j.pid) && effective[j.pid].accept)) let score = 0;
effective[j.pid] = j; let penalty_score = 0;
const _time = jdoc => jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000); let time = 0;
for (const j in journal) { if (tdoc.pids.includes(j.pid) && !(effective.includes(j.pid) && effective[j.pid].accept)) effective[j.pid] = j; }
const _time = (jdoc) => jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000);
function _penalty_score(jdoc) { function _penalty_score(jdoc) {
let exceed_seconds = jdoc.rid.generationTime - Math.floor(tdoc.penalty_since / 1000); const exceed_seconds = jdoc.rid.generationTime - Math.floor(tdoc.penalty_since / 1000);
if (exceed_seconds < 0) return jdoc.score; if (exceed_seconds < 0) return jdoc.score;
else return 0.5 * jdoc.score; return 0.5 * jdoc.score;
} }
for (let j in effective) { for (const j in effective) {
detail.push(Object.assign({}, effective[j], { detail.push({ ...effective[j], penalty_score: _penalty_score(effective[j]), time: _time(effective[j]) });
penalty_score: _penalty_score(effective[j]), time: _time(effective[j])
}));
} }
for (let d of detail) { for (const d of detail) {
score += d.score; score += d.score;
penalty_score += d.penalty_score; penalty_score += d.penalty_score;
time += d.time; time += d.time;
} }
return { score, penalty_score, time, detail }; return {
score, penalty_score, time, detail,
};
}, },
scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, dudict, pdict) { scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, dudict, pdict) {
let columns = [ const columns = [
{ 'type': 'rank', 'value': _('Rank') }, { type: 'rank', value: _('Rank') },
{ 'type': 'user', 'value': _('User') }, { type: 'user', value: _('User') },
{ 'type': 'display_name', 'value': _('Display Name') }, { type: 'display_name', value: _('Display Name') },
{ 'type': 'total_score', 'value': _('Score') } { type: 'total_score', value: _('Score') },
]; ];
if (is_export) if (is_export) {
columns.push( columns.push(
{ type: 'total_original_score', value: _('Original Score') }, { type: 'total_original_score', value: _('Original Score') },
{ type: 'total_time', value: _('Total Time (Seconds)') } { type: 'total_time', value: _('Total Time (Seconds)') },
); );
}
columns.push({ type: 'total_time_str', value: _('Total Time') }); columns.push({ type: 'total_time_str', value: _('Total Time') });
for (let i in tdoc.pids) for (const i in tdoc.pids) {
if (is_export) if (is_export) {
columns.push( columns.push(
{ type: 'problem_score', value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title) }, { type: 'problem_score', value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title) },
{ type: 'problem_original_score', value: '#{0} {1}'.format(i + 1, _('Original Score')) }, { type: 'problem_original_score', value: '#{0} {1}'.format(i + 1, _('Original Score')) },
{ type: 'problem_time', value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')) }, { type: 'problem_time', value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')) },
{ type: 'problem_time_str', value: '#{0} {1}'.format(i + 1, _('Time')) } { type: 'problem_time_str', value: '#{0} {1}'.format(i + 1, _('Time')) },
); );
else columns.push({ type: 'problem_detail', value: '#{0}'.format(i + 1), 'raw': pdict[tdoc.pids[i]] }); } else columns.push({ type: 'problem_detail', value: '#{0}'.format(i + 1), raw: pdict[tdoc.pids[i]] });
let rows = [columns]; }
for (let [rank, tsdoc] in ranked_tsdocs) { const rows = [columns];
let tsddict = {}; for (const [rank, tsdoc] in ranked_tsdocs) {
if (tsdoc.detail) const tsddict = {};
for (let item of tsdoc.detail) if (tsdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; }
tsddict[item.pid] = item; const row = [
let row = [
{ type: 'string', value: rank }, { type: 'string', value: rank },
{ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }, { type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] },
{ type: 'display_name', value: (dudict[tsdoc.uid] || {}).display_name || '' }, { type: 'display_name', value: (dudict[tsdoc.uid] || {}).display_name || '' },
{ type: 'string', value: tsdoc.penalty_score || 0 } { type: 'string', value: tsdoc.penalty_score || 0 },
]; ];
if (is_export) if (is_export) {
row.push( row.push(
{ type: 'string', value: tsdoc.score || 0 }, { type: 'string', value: tsdoc.score || 0 },
{ type: 'string', value: tsdoc.time || 0.0 } { type: 'string', value: tsdoc.time || 0.0 },
); );
}
row.push({ type: 'string', value: tsdoc.time || 0 }); row.push({ type: 'string', value: tsdoc.time || 0 });
for (let pid of tdoc.pids) { for (const pid of tdoc.pids) {
let rdoc = (tsddict[pid] || {}).rid || null; const rdoc = (tsddict[pid] || {}).rid || null;
let col_score = (tsddict[pid] || {}).penalty_score || '-'; const col_score = (tsddict[pid] || {}).penalty_score || '-';
let col_original_score = (tsddict[pid] || {}).score || '-'; const col_original_score = (tsddict[pid] || {}).score || '-';
let col_time = (tsddict[pid] || {}).time || '-'; const col_time = (tsddict[pid] || {}).time || '-';
let col_time_str = col_time != '-' ? col_time : '-'; const col_time_str = col_time != '-' ? col_time : '-';
if (is_export) row.push( if (is_export) {
{ type: 'string', value: col_score }, row.push(
{ type: 'string', value: col_original_score }, { type: 'string', value: col_score },
{ type: 'string', value: col_time }, { type: 'string', value: col_original_score },
{ type: 'string', value: col_time_str }, { type: 'string', value: col_time },
); { type: 'string', value: col_time_str },
else row.push({ );
type: 'record', } else {
value: '{0} / {1}\n{2}'.format(col_score, col_original_score, col_time_str), row.push({
raw: rdoc type: 'record',
}); value: '{0} / {1}\n{2}'.format(col_score, col_original_score, col_time_str),
raw: rdoc,
});
}
} }
rows.push(row); rows.push(row);
} }
return rows; return rows;
} },
}; };
+24 -21
View File
@@ -4,12 +4,14 @@ module.exports = {
TEXT: 'OI', TEXT: 'OI',
check: () => { }, check: () => { },
stat: (tdoc, journal) => { stat: (tdoc, journal) => {
let detail = {}, score = 0; const detail = {};
for (let j in journal) let score = 0;
for (const j in journal) {
if (tdoc.pids.includes(j.pid)) { if (tdoc.pids.includes(j.pid)) {
detail[j.pid] = j; detail[j.pid] = j;
score += j.score; score += j.score;
} }
}
return { score, detail }; return { score, detail };
}, },
showScoreboard(tdoc, now) { showScoreboard(tdoc, now) {
@@ -18,43 +20,44 @@ module.exports = {
showRecord(tdoc, now) { showRecord(tdoc, now) {
return now > tdoc.endAt; return now > tdoc.endAt;
}, },
scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, pdict) { scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
let columns = [ const columns = [
{ type: 'rank', value: _('Rank') }, { type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') }, { type: 'user', value: _('User') },
{ type: 'total_score', value: _('Total Score') } { type: 'total_score', value: _('Total Score') },
]; ];
for (let i in tdoc.pids) for (const i in tdoc.pids) {
if (is_export) if (isExport) {
columns.push({ columns.push({
type: 'problem_score', type: 'problem_score',
value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title) value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title),
}); });
else } else {
columns.push({ columns.push({
type: 'problem_detail', type: 'problem_detail',
value: '#{0}'.format(i + 1), value: '#{0}'.format(i + 1),
raw: pdict[tdoc.pids[i]] raw: pdict[tdoc.pids[i]],
}); });
let rows = [columns]; }
for (let [rank, tsdoc] of ranked_tsdocs) { }
let tsddict = {}; const rows = [columns];
if (tsdoc.journal) for (const [rank, tsdoc] of rankedTsdocs) {
for (let item of tsdoc.journal) const tsddict = {};
tsddict[item.pid] = item; if (tsdoc.journal) { for (const item of tsdoc.journal) tsddict[item.pid] = item; }
let row = []; const row = [];
row.push({ type: 'string', value: rank }); row.push({ type: 'string', value: rank });
row.push({ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }); row.push({ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] });
row.push({ type: 'string', value: tsdoc.score || 0 }); row.push({ type: 'string', value: tsdoc.score || 0 });
for (let pid of tdoc.pids) for (const pid of tdoc.pids) {
row.push({ row.push({
type: 'record', type: 'record',
value: (tsddict[pid] || {}).score || '-', value: (tsddict[pid] || {}).score || '-',
raw: (tsddict[pid] || {}).rid || null raw: (tsddict[pid] || {}).rid || null,
}); });
}
rows.push(row); rows.push(row);
} }
return rows; return rows;
}, },
rank: tdocs => ranked(tdocs, (a, b) => a.score == b.score) rank: (tdocs) => ranked(tdocs, (a, b) => a.score === b.score),
}; };
+13 -14
View File
@@ -1,9 +1,8 @@
const const fs = require('fs');
fs = require('fs'), const os = require('os');
os = require('os'), const path = require('path');
path = require('path'), const yaml = require('js-yaml');
yaml = require('js-yaml'), const { defaultsDeep } = require('lodash');
{ defaultsDeep } = require('lodash');
let options = { let options = {
db: { db: {
@@ -11,16 +10,16 @@ let options = {
host: '127.0.0.1', host: '127.0.0.1',
port: 27017, port: 27017,
username: '', username: '',
password: '' password: '',
}, },
template: { template: {
path: path.join(__dirname, '..', 'templates') path: path.join(__dirname, '..', 'templates'),
}, },
listen: { listen: {
host: '127.0.0.1', host: '127.0.0.1',
port: 8888, port: 8888,
domain: '', domain: '',
https: false https: false,
}, },
smtp: { smtp: {
from: '', from: '',
@@ -28,7 +27,7 @@ let options = {
port: 465, port: 465,
user: '', user: '',
pass: '', pass: '',
secure: false secure: false,
}, },
session: { session: {
keys: ['Hydro'], keys: ['Hydro'],
@@ -36,21 +35,21 @@ let options = {
domain: '127.0.0.1', domain: '127.0.0.1',
saved_expire_seconds: 3600 * 24, saved_expire_seconds: 3600 * 24,
unsaved_expire_seconds: 600, unsaved_expire_seconds: 600,
registration_token_expire_seconds: 600 registration_token_expire_seconds: 600,
}, },
constants: { constants: {
PROBLEM_PER_PAGE: 100, PROBLEM_PER_PAGE: 100,
RECORD_PER_PAGE: 100, RECORD_PER_PAGE: 100,
SOLUTION_PER_PAGE: 20, SOLUTION_PER_PAGE: 20,
CONTEST_PER_PAGE: 20 CONTEST_PER_PAGE: 20,
} },
}; };
try { try {
let f = path.resolve(process.cwd(), 'config.yaml'); let f = path.resolve(process.cwd(), 'config.yaml');
if (!fs.existsSync(f)) f = path.resolve(os.homedir(), '.config', 'hydro', 'config.yaml'); if (!fs.existsSync(f)) f = path.resolve(os.homedir(), '.config', 'hydro', 'config.yaml');
if (!fs.existsSync(f)) f = path.resolve('/config/config.yaml'); if (!fs.existsSync(f)) f = path.resolve('/config/config.yaml');
let t = yaml.safeLoad(fs.readFileSync(f)); const t = yaml.safeLoad(fs.readFileSync(f));
options = defaultsDeep(t, options); options = defaultsDeep(t, options);
} catch (e) { } catch (e) {
console.error('Cannot load config'); console.error('Cannot load config');
+25 -27
View File
@@ -1,4 +1,4 @@
let permissions = { const permissions = {
PERM_NONE: '0', PERM_NONE: '0',
PERM_ALL: '-', PERM_ALL: '-',
PERM_LOGGEDIN: '~', PERM_LOGGEDIN: '~',
@@ -41,32 +41,30 @@ let permissions = {
PERM_VIEW_TRAINING: 'a', PERM_VIEW_TRAINING: 'a',
PERM_CREATE_TRAINING: 'b', PERM_CREATE_TRAINING: 'b',
PERM_EDIT_TRAINING: 'c', PERM_EDIT_TRAINING: 'c',
PERM_JUDGE: 'd' PERM_JUDGE: 'd',
}; };
permissions.PERM_BASIC = permissions.PERM_BASIC = permissions.PERM_VIEW
permissions.PERM_VIEW + + permissions.PERM_VIEW_PROBLEM
permissions.PERM_VIEW_PROBLEM + + permissions.PERM_VIEW_PROBLEM_SOLUTION
permissions.PERM_VIEW_PROBLEM_SOLUTION + + permissions.PERM_VIEW_DISCUSSION
permissions.PERM_VIEW_DISCUSSION + + permissions.PERM_VIEW_CONTEST
permissions.PERM_VIEW_CONTEST + + permissions.PERM_VIEW_CONTEST_SCOREBOARD
permissions.PERM_VIEW_CONTEST_SCOREBOARD + + permissions.PERM_REGISTER_USER;
permissions.PERM_REGISTER_USER; permissions.PERM_DEFAULT = permissions.PERM_VIEW
permissions.PERM_DEFAULT = + permissions.PERM_VIEW_PROBLEM
permissions.PERM_VIEW + + permissions.PERM_SUBMIT_PROBLEM
permissions.PERM_VIEW_PROBLEM + + permissions.PERM_VIEW_PROBLEM_SOLUTION
permissions.PERM_SUBMIT_PROBLEM + + permissions.PERM_CREATE_PROBLEM_SOLUTION
permissions.PERM_VIEW_PROBLEM_SOLUTION + + permissions.PERM_VOTE_PROBLEM_SOLUTION
permissions.PERM_CREATE_PROBLEM_SOLUTION + + permissions.PERM_REPLY_PROBLEM_SOLUTION
permissions.PERM_VOTE_PROBLEM_SOLUTION + + permissions.PERM_VIEW_DISCUSSION
permissions.PERM_REPLY_PROBLEM_SOLUTION + + permissions.PERM_CREATE_DISCUSSION
permissions.PERM_VIEW_DISCUSSION + + permissions.PERM_REPLY_DISCUSSION
permissions.PERM_CREATE_DISCUSSION + + permissions.PERM_VIEW_CONTEST
permissions.PERM_REPLY_DISCUSSION + + permissions.PERM_VIEW_CONTEST_SCOREBOARD
permissions.PERM_VIEW_CONTEST + + permissions.PERM_ATTEND_CONTEST
permissions.PERM_VIEW_CONTEST_SCOREBOARD + + permissions.PERM_VIEW_TRAINING
permissions.PERM_ATTEND_CONTEST + + permissions.PERM_CREATE_TRAINING
permissions.PERM_VIEW_TRAINING + + permissions.PERM_LOGGEDIN;
permissions.PERM_CREATE_TRAINING +
permissions.PERM_LOGGEDIN;
permissions.PERM_ADMIN = permissions.PERM_ALL; permissions.PERM_ADMIN = permissions.PERM_ALL;
module.exports = permissions; module.exports = permissions;
+5 -6
View File
@@ -1,15 +1,14 @@
const EventEmitter = require('events').EventEmitter; const { EventEmitter } = require('events');
const bus = new EventEmitter(); const bus = new EventEmitter();
module.exports = { module.exports = {
subscribe(events, handler) { subscribe(events, handler) {
for (let event of events) for (const event of events) bus.on(event, handler);
bus.on(event, handler);
}, },
unsubscribe(events, handler) { unsubscribe(events, handler) {
for (let event of events) for (const event of events) bus.off(event, handler);
bus.off(event, handler);
}, },
publish(event, data) { publish(event, data) {
bus.emit(event, { value: data, event }); bus.emit(event, { value: data, event });
} },
}; };
+4 -3
View File
@@ -1,19 +1,20 @@
const Mongo = require('mongodb'); const Mongo = require('mongodb');
const options = require('../options'); const options = require('../options');
let mongourl = 'mongodb://'; let mongourl = 'mongodb://';
if (options.db.username) mongourl += options.db.username + ':' + options.db.password + '@'; if (options.db.username) mongourl += `${options.db.username}:${options.db.password}@`;
mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`; mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`;
/** /**
* @type {import('mongodb').Db} * @type {import('mongodb').Db}
*/ */
let db = null; let db = null;
Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true }) Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true })
.then(Database => { .then((Database) => {
db = Database.db(options.db.name); db = Database.db(options.db.name);
db.Db = Database; db.Db = Database;
global.bus.emit('connected'); global.bus.emit('connected');
}); });
module.exports = { module.exports = {
collection: c => db.collection(c) collection: (c) => db.collection(c),
}; };
+4 -4
View File
@@ -1,5 +1,5 @@
const const { GridFSBucket } = require('mongodb');
{ GridFSBucket } = require('mongodb'), const db = require('./db');
db = require('./db');
if (!db.s) db.s = {}; if (!db.s) db.s = {};
module.exports = new GridFSBucket(db); module.exports = new GridFSBucket(db);
+12 -13
View File
@@ -1,6 +1,6 @@
let q = {}; const q = {};
module.exports = { module.exports = {
async connect() { }, async connect() { }, // eslint-disable-line no-empty-function
async assert(queue) { async assert(queue) {
q[queue] = q[queue] || []; q[queue] = q[queue] || [];
}, },
@@ -9,20 +9,19 @@ module.exports = {
}, },
async get(queue, wait = true) { async get(queue, wait = true) {
if (wait) { if (wait) {
while (!q[queue].length) while (!q[queue].length) {
await new Promise(resolve => { await new Promise((resolve) => { // eslint-disable-line no-await-in-loop
setTimeout(resolve, 100); setTimeout(resolve, 100);
}); });
let data = q[queue][0]; }
const data = q[queue][0];
q[queue].splice(0, 1); q[queue].splice(0, 1);
return data; return data;
} else {
if (!q[queue].length) return null;
else {
let data = q[queue][0];
q[queue].splice(0, 1);
return data;
}
} }
} if (!q[queue].length) return null;
const data = q[queue][0];
q[queue].splice(0, 1);
return data;
},
}; };
+141 -101
View File
@@ -1,41 +1,42 @@
const const path = require('path');
path = require('path'), const { ObjectID } = require('bson');
{ ObjectID } = require('bson'), const Koa = require('koa');
{ MongoError } = require('mongodb'), const morgan = require('koa-morgan');
Koa = require('koa'), const Body = require('koa-body');
morgan = require('koa-morgan'), const Router = require('koa-router');
Body = require('koa-body'), const cache = require('koa-static-cache');
Router = require('koa-router'), const sockjs = require('sockjs');
cache = require('koa-static-cache'), const http = require('http');
sockjs = require('sockjs'), const https = require('https');
validator = require('../lib/validator'),
template = require('../lib/template'),
user = require('../model/user'),
blacklist = require('../model/blacklist'),
token = require('../model/token'),
opcount = require('../model/opcount'),
{ UserNotFoundError, BlacklistedError, PermissionError,
NotFoundError, UserFacingError } = require('../error');
const options = require('../options'); const options = require('../options');
let http = options.listen.https ? require('https') : require('http'); const validator = require('../lib/validator');
let app = new Koa(); const template = require('../lib/template');
let server = http.createServer(app.callback()); const user = require('../model/user');
const blacklist = require('../model/blacklist');
const token = require('../model/token');
const opcount = require('../model/opcount');
const {
UserNotFoundError, BlacklistedError, PermissionError,
NotFoundError, UserFacingError,
} = require('../error');
const app = new Koa();
const server = (options.listen.https ? https : http).createServer(app.callback());
app.keys = options.session.keys; app.keys = options.session.keys;
app.use(cache(path.join(process.cwd(), '.uibuild'), { app.use(cache(path.join(process.cwd(), '.uibuild'), {
maxAge: 365 * 24 * 60 * 60 maxAge: 365 * 24 * 60 * 60,
})); }));
app.use(Body({ app.use(Body({
multipart: true, multipart: true,
formidable: { formidable: {
maxFileSize: 256 * 1024 * 1024 maxFileSize: 256 * 1024 * 1024,
} },
})); }));
let router = new Router(); const router = new Router();
class Handler { class Handler {
/** /**
* @param {import('koa').Context} ctx * @param {import('koa').Context} ctx
*/ */
constructor(ctx) { constructor(ctx) {
this.ctx = ctx; this.ctx = ctx;
@@ -46,7 +47,7 @@ class Handler {
body: ctx.request.body, body: ctx.request.body,
files: ctx.request.files, files: ctx.request.files,
query: ctx.query, query: ctx.query,
path: ctx.path path: ctx.path,
}; };
this.response = { this.response = {
body: '', body: '',
@@ -54,23 +55,24 @@ class Handler {
status: null, status: null,
template: null, template: null,
redirect: null, redirect: null,
attachment: name => ctx.attachment(name) attachment: (name) => ctx.attachment(name),
}; };
this.UIContext = { this.UIContext = {
cdn_prefix: '/', cdn_prefix: '/',
url_prefix: '/' url_prefix: '/',
}; };
this._handler = {}; this._handler = {};
this.session = {}; this.session = {};
} }
renderHTML(name, context) { renderHTML(name, context) {
console.time(name); console.time(name);
this.hasPerm = perm => this.user.hasPerm(perm); this.hasPerm = (perm) => this.user.hasPerm(perm);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
template.render(name, Object.assign(context, { template.render(name, Object.assign(context, {
handler: this, handler: this,
_: str => str ? str.toString().translate(this.user.language) : '', _: (str) => (str ? str.toString().translate(this.user.language) : ''),
user: this.user user: this.user,
}), (error, res) => { }), (error, res) => {
console.timeEnd(name); console.timeEnd(name);
if (error) reject(error); if (error) reject(error);
@@ -78,35 +80,41 @@ class Handler {
}); });
}); });
} }
async render(name, context) { async render(name, context) {
this.response.body = await this.renderHTML(name, context); this.response.body = await this.renderHTML(name, context);
this.response.type = 'text/html'; this.response.type = 'text/html';
} }
render_title(str) {
renderTitle(str) { // eslint-disable-line class-methods-use-this
return str; return str;
} }
checkPerm(perm) {
for (let i in arguments) { checkPerm(...args) {
if (arguments[i] instanceof Array) { for (const i in args) {
if (args[i] instanceof Array) {
let p = false; let p = false;
for (let j in arguments) for (const j in args) {
if (this.user.hasPerm(arguments[i][j])) { if (this.user.hasPerm(args[i][j])) {
p = true; p = true;
break; break;
} }
if (!p) throw new PermissionError([arguments[i]]); }
} else { if (!p) throw new PermissionError([args[i]]);
if (this.user.hasPerm(arguments[i])) continue; } else if (!this.user.hasPerm(args[i])) {
else throw new PermissionError([[arguments[i]]]); throw new PermissionError([[args[i]]]);
} }
} }
} }
async limitRate(op, period_secs, max_operations) {
await opcount.inc(op, this.request.ip, period_secs, max_operations); async limitRate(op, periodSecs, maxOperations) {
await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
} }
back() { back() {
this.response.redirect = this.request.headers.referer || '/'; this.response.redirect = this.request.headers.referer || '/';
} }
async ___prepare() { async ___prepare() {
this.now = new Date(); this.now = new Date();
this._handler.sid = this.request.cookies.get('sid'); this._handler.sid = this.request.cookies.get('sid');
@@ -114,19 +122,25 @@ class Handler {
this._handler.tokenType = token.TYPE_SESSION; this._handler.tokenType = token.TYPE_SESSION;
if (this._handler.save) this._handler.expireSeconds = options.session.saved_expire_seconds; if (this._handler.save) this._handler.expireSeconds = options.session.saved_expire_seconds;
else this._handler.expireSeconds = options.session.unsaved_expire_seconds; else this._handler.expireSeconds = options.session.unsaved_expire_seconds;
this.session = this._handler.sid ? this.session = this._handler.sid
await token.update(this._handler.sid, this._handler.tokenType, this._handler.expireSeconds, { ? await token.update(
update_ip: this.request.ip, this._handler.sid,
update_ua: this.request.headers['user-agent'] || '' this._handler.tokenType,
}) : { uid: 1 }; this._handler.expireSeconds,
{
update_ip: this.request.ip,
update_ua: this.request.headers['user-agent'] || '',
},
) : { uid: 1 };
if (!this.session) this.session = { uid: 1 }; if (!this.session) this.session = { uid: 1 };
let bdoc = await blacklist.get(this.request.ip); const bdoc = await blacklist.get(this.request.ip);
if (bdoc) throw new BlacklistedError(this.request.ip); if (bdoc) throw new BlacklistedError(this.request.ip);
this.user = await user.getById(this.session.uid); this.user = await user.getById(this.session.uid);
if (!this.user) throw new UserNotFoundError(this.session.uid); if (!this.user) throw new UserNotFoundError(this.session.uid);
this.csrf_token = (await token.add(token.TYPE_CSRF_TOKEN, 600, { path: this.request.path }))[0]; [this.csrfToken] = await token.add(token.TYPE_CSRF_TOKEN, 600, { path: this.request.path });
this.preferJson = (this.request.headers['accept'] || '').includes('application/json'); this.preferJson = (this.request.headers.accept || '').includes('application/json');
} }
async ___cleanup() { async ___cleanup() {
try { try {
await this.renderBody(); await this.renderBody();
@@ -137,20 +151,24 @@ class Handler {
await this.putResponse(); await this.putResponse();
await this.saveCookie(); await this.saveCookie();
} }
async renderBody() { async renderBody() {
if (!(this.response.body || this.response.template)) { if (!(this.response.body || this.response.template)) {
this.response.body = { error: new NotFoundError() }; this.response.body = { error: new NotFoundError() };
this.response.template = 'error.html'; this.response.template = 'error.html';
} }
if (!this.preferJson) if (!this.preferJson) {
if (this.response.body || this.response.template) { if (this.response.body || this.response.template) {
if (this.request.query.noTemplate || this.preferJson) return; if (this.request.query.noTemplate || this.preferJson) return;
if (this.request.query.template || this.response.template) { const templateName = this.request.query.template || this.response.template;
if (templateName) {
this.response.body = this.response.body || {}; this.response.body = this.response.body || {};
await this.render(this.request.query.template || this.response.template, this.response.body); await this.render(templateName, this.response.body);
} }
} }
}
} }
async putResponse() { async putResponse() {
if (this.response.redirect && !this.preferJson) { if (this.response.redirect && !this.preferJson) {
this.ctx.response.type = 'application/octet-stream'; this.ctx.response.type = 'application/octet-stream';
@@ -164,27 +182,40 @@ class Handler {
if (this.response.type) this.ctx.response.type = this.response.type; if (this.response.type) this.ctx.response.type = this.response.type;
} }
} }
async saveCookie() { async saveCookie() {
if (this.session.sid) if (this.session.sid) {
await token.update(this.session.sid, this._handler.tokenType, this._handler.expireSeconds, { await token.update(
updateIp: this.request.ip, this.session.sid,
updateUa: this.request.headers['user-agent'] || '' this._handler.tokenType,
}); this._handler.expireSeconds,
else {
[this.session.sid] = await token.add(this._handler.tokenType, this._handler.expireSeconds, { updateIp: this.request.ip,
createIp: this.request.ip, updateUa: this.request.headers['user-agent'] || '',
createUa: this.request.headers['user-agent'] || '', },
updateIp: this.request.ip, );
updateUa: this.request.headers['user-agent'] || '', } else {
...this.session [this.session.sid] = await token.add(
}); this._handler.tokenType,
let cookie = { secure: options.session.secure }; this._handler.expireSeconds,
{
createIp: this.request.ip,
createUa: this.request.headers['user-agent'] || '',
updateIp: this.request.ip,
updateUa: this.request.headers['user-agent'] || '',
...this.session,
},
);
}
const cookie = { secure: options.session.secure };
if (this._handler.save) { if (this._handler.save) {
cookie.expires = this.session.expireAt, cookie.maxAge = this._handler.expireSeconds; cookie.expires = this.session.expireAt;
cookie.maxAge = this._handler.expireSeconds;
this.request.cookies.set('save', 'true', cookie); this.request.cookies.set('save', 'true', cookie);
} }
this.ctx.cookies.set('sid', this.session.sid, cookie); this.ctx.cookies.set('sid', this.session.sid, cookie);
} }
async onerror(error) { async onerror(error) {
console.error(error.message, error.params); console.error(error.message, error.params);
console.error(error.stack); console.error(error.stack);
@@ -193,12 +224,12 @@ class Handler {
await this.___cleanup().catch(() => { }); await this.___cleanup().catch(() => { });
} }
} }
function Route(route, handler) { function Route(route, RouteHandler) {
router.all(route, async (ctx) => { router.all(route, async (ctx) => {
let h = new handler(ctx); const h = new RouteHandler(ctx);
try { try {
let method = ctx.method.toLowerCase(); const method = ctx.method.toLowerCase();
let args = Object.assign({}, ctx.params, ctx.query, ctx.request.body); const args = { ...ctx.params, ...ctx.query, ...ctx.request.body };
if (args.content) validator.checkContent(args.content); if (args.content) validator.checkContent(args.content);
if (args.title) validator.checkContent(args.title); if (args.title) validator.checkContent(args.title);
@@ -207,7 +238,7 @@ function Route(route, handler) {
if (args.rid) args.rid = new ObjectID(args.rid); if (args.rid) args.rid = new ObjectID(args.rid);
if (args.tid) args.tid = new ObjectID(args.tid); if (args.tid) args.tid = new ObjectID(args.tid);
if (args.duration) args.duration = parseFloat(args.duration); if (args.duration) args.duration = parseFloat(args.duration);
if (args.pids) args.pids = args.pids.split(',').map(i => i.trim()); if (args.pids) args.pids = args.pids.split(',').map((i) => i.trim());
if (h.___prepare) await h.___prepare(args); if (h.___prepare) await h.___prepare(args);
if (h.__prepare) await h.__prepare(args); if (h.__prepare) await h.__prepare(args);
@@ -219,9 +250,10 @@ function Route(route, handler) {
if (h[`_${method}`]) await h[`_${method}`](args); if (h[`_${method}`]) await h[`_${method}`](args);
if (h[method]) await h[method](args); if (h[method]) await h[method](args);
if (method == 'post' && ctx.request.body.operation) { if (method === 'post' && ctx.request.body.operation) {
if (h[`${method}_${ctx.request.body.operation}`]) if (h[`${method}_${ctx.request.body.operation}`]) {
await h[`${method}_${ctx.request.body.operation}`](args); await h[`${method}_${ctx.request.body.operation}`](args);
}
} }
if (h.cleanup) await h.cleanup(args); if (h.cleanup) await h.cleanup(args);
@@ -236,34 +268,35 @@ function Route(route, handler) {
class ConnectionHandler { class ConnectionHandler {
/** /**
* @param {import('sockjs').Connection} conn * @param {import('sockjs').Connection} conn
*/ */
constructor(conn) { constructor(conn) {
let that = this; const that = this;
this.conn = conn; this.conn = conn;
this.request = { this.request = {
cookies: { cookies: {
get(name) { get(name) {
return that.request.cookies[name]; return that.request.cookies[name];
}, },
set() { } set() { },
}, },
params: {}, params: {},
headers: conn.headers headers: conn.headers,
}; };
this._handler = {}; this._handler = {};
let p = (conn.url.split('?')[1] || '').split('&'); const p = (conn.url.split('?')[1] || '').split('&');
for (let i in p) p[i] = p[i].split('='); for (const i in p) p[i] = p[i].split('=');
for (let i in p) this.request.params[p[i][0]] = decodeURIComponent(p[i][1]); for (const i in p) this.request.params[p[i][0]] = decodeURIComponent(p[i][1]);
} }
renderHTML(name, context) { renderHTML(name, context) {
console.time(name); console.time(name);
this.hasPerm = perm => this.user.hasPerm(perm); this.hasPerm = (perm) => this.user.hasPerm(perm);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
template.render(name, Object.assign(context, { template.render(name, Object.assign(context, {
handler: this, handler: this,
_: str => str ? str.toString().translate(this.user.language) : '', _: (str) => (str ? str.toString().translate(this.user.language) : ''),
user: this.user user: this.user,
}), (error, res) => { }), (error, res) => {
console.timeEnd(name); console.timeEnd(name);
if (error) reject(error); if (error) reject(error);
@@ -271,38 +304,43 @@ class ConnectionHandler {
}); });
}); });
} }
send(data) { send(data) {
this.conn.write(JSON.stringify(data)); this.conn.write(JSON.stringify(data));
} }
close(code, reason) { close(code, reason) {
this.conn.close(code, reason); this.conn.close(code, reason);
} }
async ___prepare() { async ___prepare() {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
this.conn.once('data', msg => { this.conn.once('data', (msg) => {
for (let i of msg.split(';')) { for (const i of msg.split(';')) {
i = i.trim().split('='); const [k, v] = i.trim().split('=');
this.request.cookies[i[0]] = i[1]; this.request.cookies[k] = v;
} }
resolve(); resolve();
}); });
setTimeout(reject, 5000); setTimeout(reject, 5000);
}); });
this._handler.sid = this.request.cookies.get('sid'); this._handler.sid = this.request.cookies.get('sid');
this.session = this._handler.sid ? await token.get(this._handler.sid, token.TYPE_SESSION) : { uid: 1 }; this.session = this._handler.sid
? await token.get(this._handler.sid, token.TYPE_SESSION)
: { uid: 1 };
if (!this.session) this.session = { uid: 1 }; if (!this.session) this.session = { uid: 1 };
let bdoc = await blacklist.get(this.request.ip); const bdoc = await blacklist.get(this.request.ip);
if (bdoc) throw new BlacklistedError(this.request.ip); if (bdoc) throw new BlacklistedError(this.request.ip);
this.user = await user.getById(this.session.uid); this.user = await user.getById(this.session.uid);
if (!this.user) throw new UserNotFoundError(this.session.uid); if (!this.user) throw new UserNotFoundError(this.session.uid);
} }
} }
function Connection(prefix, handler) { function Connection(prefix, RouteConnHandler) {
const sock = sockjs.createServer({ prefix }); const sock = sockjs.createServer({ prefix });
sock.on('connection', async conn => { sock.on('connection', async (conn) => {
let h = new handler(conn); const h = new RouteConnHandler(conn);
try { try {
let args = Object.assign({}, h.request.params); const args = { ...h.request.params };
if (args.uid) args.uid = parseInt(validator.checkUid(args.uid)); if (args.uid) args.uid = parseInt(validator.checkUid(args.uid));
if (args.page) args.page = parseInt(args.page); if (args.page) args.page = parseInt(args.page);
@@ -313,9 +351,11 @@ function Connection(prefix, handler) {
if (h.__prepare) await h.__prepare(args); if (h.__prepare) await h.__prepare(args);
if (h._prepare) await h._prepare(args); if (h._prepare) await h._prepare(args);
if (h.prepare) await h.prepare(args); if (h.prepare) await h.prepare(args);
if (h.message) conn.on('data', data => { if (h.message) {
h.message(JSON.parse(data)); conn.on('data', (data) => {
}); h.message(JSON.parse(data));
});
}
conn.on('close', async () => { conn.on('close', async () => {
if (h.cleanup) await h.cleanup(args); if (h.cleanup) await h.cleanup(args);
if (h._cleanup) await h._cleanup(args); if (h._cleanup) await h._cleanup(args);
+9 -8
View File
@@ -1,19 +1,20 @@
const const Mongo = require('mongodb');
Mongo = require('mongodb'), const options = require('./options');
options = require('./options');
async function run() { async function run() {
let mongourl = 'mongodb://'; let mongourl = 'mongodb://';
if (options.db.username) mongourl += options.db.username + ':' + options.db.password + '@'; if (options.db.username) mongourl += `${options.db.username}:${options.db.password}@`;
mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`; mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`;
let Database = await Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true }); const Database = await Mongo.MongoClient.connect(mongourl, {
let db = Database.db(options.db.name); useNewUrlParser: true, useUnifiedTopology: true,
});
const db = Database.db(options.db.name);
await db.dropDatabase(); await db.dropDatabase();
console.log('Dropped'); console.log('Dropped');
process.exit(0); process.exit(0);
} }
run().catch(e => { run().catch((e) => {
console.error(e); console.error(e);
process.exit(1); process.exit(1);
}); });
+6 -8
View File
@@ -5,13 +5,12 @@ const map = [
'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8',
'9', '0' '9', '0',
]; ];
String.random = function (digit) { String.random = function random(digit) {
let str = ''; let str = '';
for (let i = 1; i <= digit; i++) for (let i = 1; i <= digit; i++) str += map[Math.floor(Math.random() * 62)];
str += map[Math.floor(Math.random() * 62)];
return str; return str;
}; };
@@ -20,15 +19,14 @@ String.random = function (digit) {
* @param {Array} b * @param {Array} b
*/ */
Array.isDiff = function isDiff(a, b) { Array.isDiff = function isDiff(a, b) {
if (a.length != b.length) return true; if (a.length !== b.length) return true;
a.sort(); a.sort();
b.sort(); b.sort();
for (let i in a) for (const i in a) { if (a[i] !== b[i]) return true; }
if (a[i] != b[i]) return true;
return false; return false;
}; };
Date.prototype.format = function (fmt = '%Y-%m-%d %H:%M:%S') { Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') {
return fmt return fmt
.replace('%Y', this.getFullYear()) .replace('%Y', this.getFullYear())
.replace('%m', this.getMonth() + 1) .replace('%m', this.getMonth() + 1)
+5 -3
View File
@@ -13,7 +13,7 @@
"koa": "^2.11.0", "koa": "^2.11.0",
"koa-body": "^4.1.1", "koa-body": "^4.1.1",
"koa-morgan": "^1.0.1", "koa-morgan": "^1.0.1",
"koa-router": "^7.4.0", "koa-router": "^8.0.8",
"koa-static-cache": "^5.1.2", "koa-static-cache": "^5.1.2",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"markdown-it": "^10.0.0", "markdown-it": "^10.0.0",
@@ -24,7 +24,9 @@
"sockjs": "^0.3.20" "sockjs": "^0.3.20"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^6.6.0" "eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.1.0",
"eslint-plugin-import": "^2.20.2"
}, },
"scripts": { "scripts": {
"build": "webpack --config webpack.config.js", "build": "webpack --config webpack.config.js",
@@ -44,4 +46,4 @@
"bin": { "bin": {
"judger": "hydro/development.js" "judger": "hydro/development.js"
} }
} }
@@ -88,7 +88,7 @@
<input type="button" class="rounded button nojs--hide" name="dczcomments__box__cancel" value="{{ _('Cancel') }} (Esc)"> <input type="button" class="rounded button nojs--hide" name="dczcomments__box__cancel" value="{{ _('Cancel') }} (Esc)">
</div> </div>
<input type="hidden" name="operation" value="{{ comment_post_op }}"> <input type="hidden" name="operation" value="{{ comment_post_op }}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
</form> </form>
</div> </div>
<div class="nojs--hide"> <div class="nojs--hide">
+3 -3
View File
@@ -54,7 +54,7 @@
<input type="button" class="rounded button nojs--hide" name="dczcomments__box__cancel" value="{{ _('Cancel') }} (Esc)"> <input type="button" class="rounded button nojs--hide" name="dczcomments__box__cancel" value="{{ _('Cancel') }} (Esc)">
</div> </div>
<input type="hidden" name="operation" value="{{ comment_post_op }}"> <input type="hidden" name="operation" value="{{ comment_post_op }}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
</form> </form>
</div> </div>
<div class="nojs--hide"> <div class="nojs--hide">
@@ -91,13 +91,13 @@
<form method="post"> <form method="post">
<input type="hidden" name="operation" value="upvote"> <input type="hidden" name="operation" value="upvote">
<input type="hidden" name="psid" value="{{ doc._id }}"> <input type="hidden" name="psid" value="{{ doc._id }}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="vote-button upvote{% if pssdict[doc._id].vote == 1 %} active{% endif %}" data-tooltip="{{ _('Upvote') }}" data-tooltip-pos="bottom center" aria-label="{{ _('Upvote') }}"><span class="icon icon-vote--up"></span></button> <button type="submit" class="vote-button upvote{% if pssdict[doc._id].vote == 1 %} active{% endif %}" data-tooltip="{{ _('Upvote') }}" data-tooltip-pos="bottom center" aria-label="{{ _('Upvote') }}"><span class="icon icon-vote--up"></span></button>
</form> </form>
<form method="post"> <form method="post">
<input type="hidden" name="operation" value="downvote"> <input type="hidden" name="operation" value="downvote">
<input type="hidden" name="psid" value="{{ doc._id }}"> <input type="hidden" name="psid" value="{{ doc._id }}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="vote-button downvote{% if pssdict[doc._id].vote == -1 %} active{% endif %}" data-tooltip="{{ _('Downvote') }}" data-tooltip-pos="bottom center" aria-label="{{ _('Downvote') }}"><span class="icon icon-vote--down"></span></button> <button type="submit" class="vote-button downvote{% if pssdict[doc._id].vote == -1 %} active{% endif %}" data-tooltip="{{ _('Downvote') }}" data-tooltip-pos="bottom center" aria-label="{{ _('Downvote') }}"><span class="icon icon-vote--down"></span></button>
</form> </form>
</div> </div>
+1 -1
View File
@@ -64,7 +64,7 @@
markdown:true markdown:true
}) }} }) }}
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
{% if page_name == 'contest_edit' %} {% if page_name == 'contest_edit' %}
<button type="submit" class="rounded primary button"> <button type="submit" class="rounded primary button">
{{ _('Update') }} {{ _('Update') }}
+1 -1
View File
@@ -10,7 +10,7 @@
<form method="post"> <form method="post">
{% include "partials/discussion_edit_form.html" %} {% include "partials/discussion_edit_form.html" %}
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="rounded primary button"> <button type="submit" class="rounded primary button">
{{ _('Create') }} (Ctrl+Enter) {{ _('Create') }} (Ctrl+Enter)
</button> </button>
+1 -1
View File
@@ -40,7 +40,7 @@
{% if handler.hasPerm(perm.PERM_LOGGEDIN) %} {% if handler.hasPerm(perm.PERM_LOGGEDIN) %}
<li><form class="form--inline" method="post"> <li><form class="form--inline" method="post">
<input type="hidden" name="operation" value="{% if not dsdoc['star'] %}star{% else %}unstar{% endif %}"> <input type="hidden" name="operation" value="{% if not dsdoc['star'] %}star{% else %}unstar{% endif %}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="star{% if dsdoc['star'] %} activated{% endif %}" type="submit"> <button class="star{% if dsdoc['star'] %} activated{% endif %}" type="submit">
<span class="starred--hide"><span class="icon icon-star--outline"></span> {{ _('Star Topic') }}</span> <span class="starred--hide"><span class="icon icon-star--outline"></span> {{ _('Star Topic') }}</span>
<span class="starred--show"><span class="icon icon-star"></span> {{ _('Unstar Topic') }}</span> <span class="starred--show"><span class="icon icon-star"></span> {{ _('Unstar Topic') }}</span>
+1 -1
View File
@@ -7,7 +7,7 @@
<form method="post"> <form method="post">
{% include "partials/discussion_edit_form.html" %} {% include "partials/discussion_edit_form.html" %}
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button name="operation" value="update" type="submit" class="rounded primary button"> <button name="operation" value="update" type="submit" class="rounded primary button">
{{ _('Update') }} (Ctrl+Enter) {{ _('Update') }} (Ctrl+Enter)
</button> </button>
+1 -1
View File
@@ -13,7 +13,7 @@
</div> </div>
<div class="section__body typo"> <div class="section__body typo">
<form method="post"> <form method="post">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
{% if join_settings['method'] == vj4.constant.domain.JOIN_METHOD_CODE %} {% if join_settings['method'] == vj4.constant.domain.JOIN_METHOD_CODE %}
{{ form.form_text(columns=None, label='Invitation Code', name='code', required=true, help_text='You need to enter the invitation code to join the domain.', value=code, autofocus=True) }} {{ form.form_text(columns=None, label='Invitation Code', name='code', required=true, help_text='You need to enter the invitation code to join the domain.', value=code, autofocus=True) }}
{% endif %} {% endif %}
+1 -1
View File
@@ -6,7 +6,7 @@
<div class="section__tools"> <div class="section__tools">
<form method="post"> <form method="post">
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="rounded primary button"> <button type="submit" class="rounded primary button">
<span class="icon icon-add"></span> {{ _('Initialize') }} <span class="icon icon-add"></span> {{ _('Initialize') }}
</button> </button>
+1 -1
View File
@@ -7,7 +7,7 @@
{% include "partials/domain_edit_form.html" %} {% include "partials/domain_edit_form.html" %}
{% endwith %} {% endwith %}
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="rounded primary button"> <button type="submit" class="rounded primary button">
{{ _('Update') }} {{ _('Update') }}
</button> </button>
@@ -50,7 +50,7 @@
name='invitation_code', name='invitation_code',
help_text='The invitation code to enter to successfully join the domain. You can only use letters and numbers in the code and it should not be longer than 64 characters.', help_text='The invitation code to enter to successfully join the domain. You can only use letters and numbers in the code and it should not be longer than 64 characters.',
value=join_settings['code']|default('')) }} value=join_settings['code']|default('')) }}
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="rounded primary button" role="submit"> <button class="rounded primary button" role="submit">
{{ _('Update Settings') }} {{ _('Update Settings') }}
</button> </button>
+1 -1
View File
@@ -47,7 +47,7 @@
</table> </table>
</div> </div>
<div class="section__body"> <div class="section__body">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="rounded primary button" role="submit"> <button class="rounded primary button" role="submit">
{{ _('Update Permission') }} {{ _('Update Permission') }}
</button> </button>
+1 -1
View File
@@ -10,7 +10,7 @@
<p>{{ _('Image Markdown') }}: <pre>![{{ _('Description') }}](vijos:/{{ reverse_url('fs_get', domain_id=vj4.model.builtin.DOMAIN_ID_SYSTEM, secret=fdoc['metadata']['secret']) }})</pre></p> <p>{{ _('Image Markdown') }}: <pre>![{{ _('Description') }}](vijos:/{{ reverse_url('fs_get', domain_id=vj4.model.builtin.DOMAIN_ID_SYSTEM, secret=fdoc['metadata']['secret']) }})</pre></p>
{% endif %} {% endif %}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<p> <p>
{{ _('New file') }}: {{ _('New file') }}:
<input type="text" name="desc" placeholder="{{ _('Description') }}"> <input type="text" name="desc" placeholder="{{ _('Description') }}">
+1 -1
View File
@@ -47,7 +47,7 @@
</td> </td>
<td class="col--action"><form method="post"> <td class="col--action"><form method="post">
<input type="hidden" name="domain_id" value="{{ ddoc['_id'] }}"> <input type="hidden" name="domain_id" value="{{ ddoc['_id'] }}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="typo-a">{{ _('Continue') }}</button> <button type="submit" class="typo-a">{{ _('Continue') }}</button>
</form></td> </form></td>
</tr> </tr>
+1 -1
View File
@@ -21,7 +21,7 @@
{% include "partials/domain_edit_form.html" %} {% include "partials/domain_edit_form.html" %}
{% endwith %} {% endwith %}
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="rounded primary button"> <button type="submit" class="rounded primary button">
{{ _('Create') }} {{ _('Create') }}
</button> </button>
+4 -4
View File
@@ -13,7 +13,7 @@
{{ form.form_text(type='text', label='Current Email', columns=10, name='current_email', value=handler.user['mail'], disabled=true) }} {{ form.form_text(type='text', label='Current Email', columns=10, name='current_email', value=handler.user['mail'], disabled=true) }}
{{ form.form_text(type='text', label='New Email', columns=10, name='mail', required=true) }} {{ form.form_text(type='text', label='New Email', columns=10, name='mail', required=true) }}
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="hidden" name="operation" value="change_mail"> <input type="hidden" name="operation" value="change_mail">
<input type="submit" class="rounded primary button" value="{{ _('Change Email') }}"> <input type="submit" class="rounded primary button" value="{{ _('Change Email') }}">
</div></div> </div></div>
@@ -32,7 +32,7 @@
{{ form.form_text(type='password', label='New Password', columns=10, name='new_password', required=true) }} {{ form.form_text(type='password', label='New Password', columns=10, name='new_password', required=true) }}
{{ form.form_text(type='password', label='Repeat Password', columns=10, name='verify_password', required=true) }} {{ form.form_text(type='password', label='Repeat Password', columns=10, name='verify_password', required=true) }}
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="hidden" name="operation" value="change_password"> <input type="hidden" name="operation" value="change_password">
<input type="submit" class="rounded primary button" value="{{ _('Change Password') }}"> <input type="submit" class="rounded primary button" value="{{ _('Change Password') }}">
</div></div> </div></div>
@@ -72,7 +72,7 @@
<input type="hidden" name="operation" value="delete_token"> <input type="hidden" name="operation" value="delete_token">
<input type="hidden" name="token_type" value="{{ session['token_type'] }}"> <input type="hidden" name="token_type" value="{{ session['token_type'] }}">
<input type="hidden" name="token_digest" value="{{ session['token_digest'] }}"> <input type="hidden" name="token_digest" value="{{ session['token_digest'] }}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="submit" value="{{ _('Logout This Session') }}" class="rounded button"> <input type="submit" value="{{ _('Logout This Session') }}" class="rounded button">
</form> </form>
</div> </div>
@@ -85,7 +85,7 @@
<div class="section__body"> <div class="section__body">
<form method="post"> <form method="post">
<input type="hidden" name="operation" value="delete_all_tokens"> <input type="hidden" name="operation" value="delete_all_tokens">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="submit" value="{{ _('Logout All Sessions') }}" class="primary rounded button"> <input type="submit" value="{{ _('Logout All Sessions') }}" class="primary rounded button">
</form> </form>
<div class="typo"> <div class="typo">
+1 -1
View File
@@ -61,7 +61,7 @@
</div> </div>
{% endfor %} {% endfor %}
<div class="section__body"> <div class="section__body">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="submit" class="rounded primary button" value="{{ _('Save All Changes') }}"> <input type="submit" class="rounded primary button" value="{{ _('Save All Changes') }}">
</div> </div>
</form> </form>
+1 -1
View File
@@ -23,7 +23,7 @@
{{ form.form_text(columns=None, label='Problems', name='pids', value=pids) }} {{ form.form_text(columns=None, label='Problems', name='pids', value=pids) }}
{{ form.form_textarea(columns=None, label='Content', name='content', value=tdoc['content']|default(''), markdown=True) }} {{ form.form_textarea(columns=None, label='Content', name='content', value=tdoc['content']|default(''), markdown=True) }}
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
{% if page_name == 'homework_edit' %} {% if page_name == 'homework_edit' %}
<button type="submit" class="rounded primary button"> <button type="submit" class="rounded primary button">
{{ _('Update') }} {{ _('Update') }}
+1 -1
View File
@@ -23,7 +23,7 @@
<meta name="theme-color" content="#56758f"> <meta name="theme-color" content="#56758f">
<link rel="stylesheet" media="all" href="{{ static_url('vj4.css') }}"> <link rel="stylesheet" media="all" href="{{ static_url('vj4.css') }}">
<title>{{ handler.render_title(page_title) }}</title> <title>{{ handler.renderTitle(page_title) }}</title>
<script> <script>
var _htmlNode = document.documentElement; var _htmlNode = document.documentElement;
_htmlNode.className = _htmlNode.className.replace(' nojs', ' hasjs'); _htmlNode.className = _htmlNode.className.replace(' nojs', ' hasjs');
+1 -1
View File
@@ -27,7 +27,7 @@
{% if handler.hasPerm(perm.PERM_ATTEND_CONTEST) and handler.hasPerm(perm.PERM_LOGGEDIN) %} {% if handler.hasPerm(perm.PERM_ATTEND_CONTEST) and handler.hasPerm(perm.PERM_LOGGEDIN) %}
<form action="/c/{{ tdoc._id }}" method="POST"> <form action="/c/{{ tdoc._id }}" method="POST">
<input type="hidden" name="operation" value="attend"> <input type="hidden" name="operation" value="attend">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="menu__link" type="submit"> <button class="menu__link" type="submit">
<span class="icon icon-add"></span> {{ _('Attend Contest') }} <span class="icon icon-add"></span> {{ _('Attend Contest') }}
</button> </button>
+1 -1
View File
@@ -27,7 +27,7 @@
{% if handler.hasPerm(perm.PERM_ATTEND_HOMEWORK) and handler.hasPerm(perm.PERM_LOGGEDIN) %} {% if handler.hasPerm(perm.PERM_ATTEND_HOMEWORK) and handler.hasPerm(perm.PERM_LOGGEDIN) %}
<form action="{{ reverse_url('homework_detail', tid=tdoc['doc_id']) }}" method="POST"> <form action="{{ reverse_url('homework_detail', tid=tdoc['doc_id']) }}" method="POST">
<input type="hidden" name="operation" value="attend"> <input type="hidden" name="operation" value="attend">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="menu__link" type="submit"> <button class="menu__link" type="submit">
<span class="icon icon-add"></span> {{ _('Claim Homework') }} <span class="icon icon-add"></span> {{ _('Claim Homework') }}
</button> </button>
+1 -1
View File
@@ -37,7 +37,7 @@
</label> </label>
</div></div> </div></div>
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
{% if page_name == 'problem_edit' %} {% if page_name == 'problem_edit' %}
<button type="submit" class="rounded primary button"> <button type="submit" class="rounded primary button">
{{ _('Update') }} {{ _('Update') }}
+1 -1
View File
@@ -37,7 +37,7 @@
</label> </label>
</div></div> </div></div>
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="rounded primary button"> <button type="submit" class="rounded primary button">
{{ _('Copy') }} {{ _('Copy') }}
</button> </button>
+1 -1
View File
@@ -42,7 +42,7 @@
value:pdoc['tag']|join(', ') value:pdoc['tag']|join(', ')
}) }} }) }}
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="rounded primary button"> <button type="submit" class="rounded primary button">
{{ _('Update') }} {{ _('Update') }}
</button> </button>
+1 -1
View File
@@ -52,7 +52,7 @@
}) }} }) }}
{{ form.form_textarea({label:'Code', columns:12, name:'code', extra_class:'monospace', autofocus:true}) }} {{ form.form_textarea({label:'Code', columns:12, name:'code', extra_class:'monospace', autofocus:true}) }}
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="submit" class="rounded primary button" value="{{ _('Submit') }}"> <input type="submit" class="rounded primary button" value="{{ _('Submit') }}">
</div></div> </div></div>
</form> </form>
+1 -1
View File
@@ -8,7 +8,7 @@
<p>{{ _('MD5: {0}').format(md5 or _('(None)')) }}</p> <p>{{ _('MD5: {0}').format(md5 or _('(None)')) }}</p>
{% endif %} {% endif %}
<p>{{ _('New dataset') }}: <p>{{ _('New dataset') }}:
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="file" name="file"> <input type="file" name="file">
<input type="submit" value="{{ _('Upload') }}" class="rounded primary button"> <input type="submit" value="{{ _('Upload') }}" class="rounded primary button">
</p> </p>
+2 -2
View File
@@ -33,7 +33,7 @@
<ol class="menu"> <ol class="menu">
<li class="menu__item"> <li class="menu__item">
<form method="post" action="/r/{{ rdoc._id }}/rejudge"> <form method="post" action="/r/{{ rdoc._id }}/rejudge">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="menu__link"> <button type="submit" class="menu__link">
<span class="icon icon-refresh"></span> {{ _('Rejudge') }} <span class="icon icon-refresh"></span> {{ _('Rejudge') }}
</button> </button>
@@ -42,7 +42,7 @@
<li class="menu__item"> <li class="menu__item">
<form method="post" action="/judge/end"> <form method="post" action="/judge/end">
<input type="hidden" name="rid" value="{{ rdoc._id }}"> <input type="hidden" name="rid" value="{{ rdoc._id }}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="hidden" name="score" value="0"> <input type="hidden" name="score" value="0">
<input type="hidden" name="message" value="{{ _('score canceled') }}"> <input type="hidden" name="message" value="{{ _('score canceled') }}">
<button type="submit" class="menu__link"> <button type="submit" class="menu__link">
+1 -1
View File
@@ -5,7 +5,7 @@
<td class="col--problem col--problem-name"> <td class="col--problem col--problem-name">
{% if handler.hasPerm(perm.PERM_REJUDGE) %} {% if handler.hasPerm(perm.PERM_REJUDGE) %}
<form class="form--inline" method="post" action="/r/{{ rdoc._id }}/rejudge"> <form class="form--inline" method="post" action="/r/{{ rdoc._id }}/rejudge">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="link text-maroon lighter"> <button type="submit" class="link text-maroon lighter">
<span class="icon icon-refresh"></span> <span class="icon icon-refresh"></span>
{{ _('Rejudge') }} {{ _('Rejudge') }}
+2 -2
View File
@@ -115,7 +115,7 @@
<form class="form--inline" action="{{ reverse_url('problem_main') }}" method="post"> <form class="form--inline" action="{{ reverse_url('problem_main') }}" method="post">
<input type="hidden" name="pid" value="{{ pdoc['_id'] }}"> <input type="hidden" name="pid" value="{{ pdoc['_id'] }}">
<input type="hidden" name="operation" value="{% if not psdoc['star'] %}star{% else %}unstar{% endif %}"> <input type="hidden" name="operation" value="{% if not psdoc['star'] %}star{% else %}unstar{% endif %}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="star{% if psdoc['star'] %} activated{% endif %}" type="submit"> <button class="star{% if psdoc['star'] %} activated{% endif %}" type="submit">
<span class="starred--hide"><span class="icon icon-star--outline" data-tooltip="{{ _('Star') }}"></span></span> <span class="starred--hide"><span class="icon icon-star--outline" data-tooltip="{{ _('Star') }}"></span></span>
<span class="starred--show"><span class="icon icon-star" data-tooltip="{{ _('Unstar') }}"></span></span> <span class="starred--show"><span class="icon icon-star" data-tooltip="{{ _('Unstar') }}"></span></span>
@@ -168,7 +168,7 @@
<li class="menu__item"> <li class="menu__item">
<form action="" method="POST"> <form action="" method="POST">
<input type="hidden" name="operation" value="enroll"> <input type="hidden" name="operation" value="enroll">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="menu__link" type="submit"> <button class="menu__link" type="submit">
<span class="icon icon-add"></span> {{ _('Enroll Training') }} <span class="icon icon-add"></span> {{ _('Enroll Training') }}
</button> </button>
+1 -1
View File
@@ -33,7 +33,7 @@
</label> </label>
</div></div> </div></div>
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
{% if page_name == 'training_edit' %} {% if page_name == 'training_edit' %}
<button type="submit" class="rounded primary button"> <button type="submit" class="rounded primary button">
{{ _('Update') }} {{ _('Update') }}
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="immersive--content immersive--center"> <div class="immersive--content immersive--center">
<h1>{{ _('Logout') }}</h1> <h1>{{ _('Logout') }}</h1>
<form method="POST"> <form method="POST">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}"> <input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<div class="text-center"> <div class="text-center">
<input type="submit" value="{{ _('Logout') }}" class="inverse rounded primary button"> <input type="submit" value="{{ _('Logout') }}" class="inverse rounded primary button">
</div> </div>
+616 -173
View File
File diff suppressed because it is too large. Load diff