mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
添加功能:pretest discuss user dashboard statistics
This commit is contained in:
1 parent
61cba6f180
commit
15d43636c0
41 files changed
+920
-523
No files matched your search
+2
-1
@@ -24,6 +24,7 @@ module.exports = {
|
|||||||
"radix": "off",
|
"radix": "off",
|
||||||
"guard-for-in": "off",
|
"guard-for-in": "off",
|
||||||
"no-param-reassign": "off",
|
"no-param-reassign": "off",
|
||||||
"global-require": "off"
|
"global-require": "off",
|
||||||
|
"no-nested-ternary": "off",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
config.yaml
|
config.yaml
|
||||||
|
.build/
|
||||||
.uibuild/
|
.uibuild/
|
||||||
__*
|
__*
|
||||||
ui/misc/.iconfont
|
ui/misc/.iconfont
|
||||||
ui/static/locale
|
ui/static/locale
|
||||||
.webpackStats.json
|
|
||||||
.cache
|
.cache
|
||||||
dist/
|
.webpackStats.json
|
||||||
Vendored
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"files.autoGuessEncoding": false,
|
||||||
"files.autoSave": "onFocusChange",
|
"files.autoSave": "onFocusChange",
|
||||||
"files.encoding": "utf8",
|
"files.encoding": "utf8",
|
||||||
"files.autoGuessEncoding": false,
|
"files.eol": "\n",
|
||||||
"editor.detectIndentation": false,
|
"editor.detectIndentation": false,
|
||||||
"editor.formatOnSave": false
|
"editor.formatOnSave": false
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const root = require('./root');
|
||||||
|
try {
|
||||||
|
fs.rmdirSync(root('.build'));
|
||||||
|
} catch (e) { }
|
||||||
|
fs.mkdirSync(root('.build'));
|
||||||
|
let progress = (name) => ((prog) => {
|
||||||
|
console.log(name, prog);
|
||||||
|
});
|
||||||
|
require('./locales')('locales', progress);
|
||||||
|
require('./webpack')('webpack', progress);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
const yaml = require('js-yaml');
|
||||||
|
const fs = require('fs');
|
||||||
|
const root = require('./root');
|
||||||
|
const build = async (next) => {
|
||||||
|
let langs = fs.readdirSync(root('locales'));
|
||||||
|
next({ total: langs.length });
|
||||||
|
let lang = {};
|
||||||
|
let count = 0;
|
||||||
|
for (let i of langs) {
|
||||||
|
const content = fs.readFileSync(root(`locales/${i}`)).toString();
|
||||||
|
lang[i.split('.')[0]] = yaml.safeLoad(content);
|
||||||
|
next({ progress: ++count });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(root('.build/locales.json'), JSON.stringify(lang));
|
||||||
|
}
|
||||||
|
module.exports = build;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const path = require('path');
|
||||||
|
function root(name) {
|
||||||
|
return path.resolve(__dirname, '..', name);
|
||||||
|
}
|
||||||
|
module.exports = root;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const root = require('./root');
|
||||||
|
const build = async (next) => {
|
||||||
|
const modules = fs.readdirSync(root('hydro', 'module'));
|
||||||
|
const config = {
|
||||||
|
mode: 'production',
|
||||||
|
entry: {
|
||||||
|
development: root('hydro/development.js'),
|
||||||
|
install: root('hydro/install.js'),
|
||||||
|
uninstall: root('hydro/uninstall.js')
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: '[name].js',
|
||||||
|
path: root('.build')
|
||||||
|
},
|
||||||
|
target: 'node',
|
||||||
|
module: {}
|
||||||
|
};
|
||||||
|
for (let i of modules) {
|
||||||
|
if (fs.statSync(path.resolve(__dirname, 'hydro', 'module', i)).isDirectory()) {
|
||||||
|
config.entry[root(`.build/module/${i}`)] = root(`./hydro/module/${i}/index.js`);
|
||||||
|
} else {
|
||||||
|
config.entry[root(`.build/module/${i}`)] = root(`./hydro/module/${i}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const compiler = webpack(config);
|
||||||
|
function compilerCallback(err, stats) {
|
||||||
|
if (err) {
|
||||||
|
console.error(err.stack || err);
|
||||||
|
if (err.details) console.error(err.details);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!watch && stats.hasErrors()) process.exitCode = 1;
|
||||||
|
compiler.run(compilerCallback);
|
||||||
|
next({ total: 100 });
|
||||||
|
}
|
||||||
|
module.exports = build;
|
||||||
+10
-19
@@ -8,25 +8,24 @@ process.stdin.on('data', async (input) => {
|
|||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const path = require('path');
|
|
||||||
const EventEmitter = require('events');
|
|
||||||
const i18n = require('./lib/i18n');
|
|
||||||
|
|
||||||
i18n(path.resolve(__dirname, '..', 'locales', 'zh_CN.yaml'), 'zh_CN');
|
require('./lib/i18n');
|
||||||
i18n(path.resolve(__dirname, '..', 'locales', 'zh_TW.yaml'), 'zh_TW');
|
require('./utils');
|
||||||
i18n(path.resolve(__dirname, '..', 'locales', 'en.yaml'), 'en');
|
|
||||||
|
const bus = require('./service/bus');
|
||||||
|
|
||||||
global.bus = new EventEmitter();
|
|
||||||
async function run() {
|
async function run() {
|
||||||
require('./utils');
|
|
||||||
require('./service/db');
|
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
global.bus.once('connected', () => {
|
const h = () => {
|
||||||
console.log('Database connected');
|
console.log('Database connected');
|
||||||
|
bus.unsubscribe(['system_database_connected'], h);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
};
|
||||||
|
bus.subscribe(['system_database_connected'], h);
|
||||||
|
require('./service/db');
|
||||||
});
|
});
|
||||||
require('./service/gridfs');
|
require('./service/gridfs');
|
||||||
|
require('./service/queue');
|
||||||
const server = require('./service/server');
|
const server = require('./service/server');
|
||||||
require('./handler/home');
|
require('./handler/home');
|
||||||
require('./handler/problem');
|
require('./handler/problem');
|
||||||
@@ -38,14 +37,6 @@ async function run() {
|
|||||||
require('./handler/import');
|
require('./handler/import');
|
||||||
server.start();
|
server.start();
|
||||||
}
|
}
|
||||||
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) => {
|
run().catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
+65
-94
@@ -6,6 +6,7 @@ class UserFacingError extends Error {
|
|||||||
this.params = [];
|
this.params = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SystemError extends Error {
|
class SystemError extends Error {
|
||||||
constructor(type) {
|
constructor(type) {
|
||||||
super(type);
|
super(type);
|
||||||
@@ -14,70 +15,89 @@ class SystemError extends Error {
|
|||||||
this.params = [];
|
this.params = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BadRequestError extends UserFacingError {
|
class BadRequestError extends UserFacingError {
|
||||||
constructor(type) {
|
constructor(type) {
|
||||||
super(type);
|
super(type);
|
||||||
this.code = 400;
|
this.code = 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ForbiddenError extends UserFacingError {
|
class ForbiddenError extends UserFacingError {
|
||||||
constructor(type) {
|
constructor(type) {
|
||||||
super(type);
|
super(type);
|
||||||
this.code = 403;
|
this.code = 403;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotFoundError extends UserFacingError {
|
class NotFoundError extends UserFacingError {
|
||||||
constructor(type) {
|
constructor(type) {
|
||||||
super(type || 'NotFoundError');
|
super(type || 'NotFoundError');
|
||||||
this.code = 404;
|
this.code = 404;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RemoteOnlineJudgeError extends UserFacingError {
|
class RemoteOnlineJudgeError extends UserFacingError {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super('RemoteOnlineJudgeError');
|
super('RemoteOnlineJudgeError');
|
||||||
this.params = [message];
|
this.params = [message];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlreadyVotedError extends BadRequestError {
|
class AlreadyVotedError extends BadRequestError {
|
||||||
constructor(psid, uid) {
|
constructor(psid, uid) {
|
||||||
super('You\'ve already voted.');
|
super('You\'ve already voted.');
|
||||||
this.params = [psid, uid];
|
this.params = [psid, uid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoginError extends ForbiddenError {
|
class LoginError extends ForbiddenError {
|
||||||
constructor(uname) {
|
constructor(uname) {
|
||||||
super('LoginError');
|
super('LoginError');
|
||||||
this.params = [uname];
|
this.params = [uname];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserAlreadyExistError extends ForbiddenError {
|
class UserAlreadyExistError extends ForbiddenError {
|
||||||
constructor(uname) {
|
constructor(uname) {
|
||||||
super('UserAlreadyExistError');
|
super('UserAlreadyExistError');
|
||||||
this.params = [uname];
|
this.params = [uname];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InvalidTokenError extends ForbiddenError {
|
class InvalidTokenError extends ForbiddenError {
|
||||||
constructor(token) {
|
constructor(token) {
|
||||||
super('InvalidTokenError');
|
super('InvalidTokenError');
|
||||||
this.params = [token];
|
this.params = [token];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BlacklistedError extends ForbiddenError {
|
||||||
|
constructor(ip) {
|
||||||
|
super('Address {0} is blacklisted.');
|
||||||
|
this.params = [ip];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class UserNotFoundError extends NotFoundError {
|
class UserNotFoundError extends NotFoundError {
|
||||||
constructor(user) {
|
constructor(user) {
|
||||||
super('UserNotFoundError');
|
super('UserNotFoundError');
|
||||||
this.params = [user];
|
this.params = [user];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoProblemError extends NotFoundError {
|
class NoProblemError extends NotFoundError {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('NoProblemError');
|
super('NoProblemError');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VerifyPasswordError extends ForbiddenError {
|
class VerifyPasswordError extends ForbiddenError {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('VerifyPasswordError');
|
super('VerifyPasswordError');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class OpcountExceededError extends ForbiddenError {
|
class OpcountExceededError extends ForbiddenError {
|
||||||
constructor(op, periodSecs, maxOperations) {
|
constructor(op, periodSecs, maxOperations) {
|
||||||
super('OpcountExceededError');
|
super('OpcountExceededError');
|
||||||
@@ -91,6 +111,7 @@ class PermissionError extends ForbiddenError {
|
|||||||
this.params = [perm];
|
this.params = [perm];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ValidationError extends ForbiddenError {
|
class ValidationError extends ForbiddenError {
|
||||||
constructor(field0, field1) {
|
constructor(field0, field1) {
|
||||||
super('ValidationError');
|
super('ValidationError');
|
||||||
@@ -98,66 +119,84 @@ class ValidationError extends ForbiddenError {
|
|||||||
else this.params = [field0, field1];
|
else this.params = [field0, field1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContestNotAttendedError extends ForbiddenError {
|
class ContestNotAttendedError extends ForbiddenError {
|
||||||
constructor(tid) {
|
constructor(tid) {
|
||||||
super('You haven\'t attended this contest yet.');
|
super('You haven\'t attended this contest yet.');
|
||||||
this.params = [tid];
|
this.params = [tid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContestAlreadyAttendedError extends ForbiddenError {
|
class ContestAlreadyAttendedError extends ForbiddenError {
|
||||||
constructor(tid, uid) {
|
constructor(tid, uid) {
|
||||||
super('You\'ve already attended this contest.');
|
super('You\'ve already attended this contest.');
|
||||||
this.params = [tid, uid];
|
this.params = [tid, uid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContestNotLiveError extends ForbiddenError {
|
class ContestNotLiveError extends ForbiddenError {
|
||||||
constructor(tid) {
|
constructor(tid) {
|
||||||
super('This contest is not live.');
|
super('This contest is not live.');
|
||||||
this.params = [tid];
|
this.params = [tid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContestScoreboardHiddenError extends ForbiddenError {
|
class ContestScoreboardHiddenError extends ForbiddenError {
|
||||||
constructor(tid) {
|
constructor(tid) {
|
||||||
super('Contest scoreboard is not visible.');
|
super('Contest scoreboard is not visible.');
|
||||||
this.params = [tid];
|
this.params = [tid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TrainingAlreadyEnrollError extends ForbiddenError {
|
class TrainingAlreadyEnrollError extends ForbiddenError {
|
||||||
constructor(tid, uid) {
|
constructor(tid, uid) {
|
||||||
super("You've already enrolled this training.");
|
super("You've already enrolled this training.");
|
||||||
this.params = [tid, uid];
|
this.params = [tid, uid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RoleAlreadyExistError extends ForbiddenError {
|
||||||
|
constructor(role) {
|
||||||
|
super('This role already exists.');
|
||||||
|
this.params = [role];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ProblemNotFoundError extends NotFoundError {
|
class ProblemNotFoundError extends NotFoundError {
|
||||||
constructor(pid) {
|
constructor(pid) {
|
||||||
super('ProblemNotFoundError');
|
super('ProblemNotFoundError');
|
||||||
this.params = [pid];
|
this.params = [pid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RecordNotFoundError extends NotFoundError {
|
class RecordNotFoundError extends NotFoundError {
|
||||||
constructor(rid) {
|
constructor(rid) {
|
||||||
super('RecordNotFoundError');
|
super('RecordNotFoundError');
|
||||||
this.params = [rid];
|
this.params = [rid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SolutionNotFoundError extends NotFoundError {
|
class SolutionNotFoundError extends NotFoundError {
|
||||||
constructor(psid) {
|
constructor(psid) {
|
||||||
super('SolutionNotFoundError');
|
super('SolutionNotFoundError');
|
||||||
this.params = [psid];
|
this.params = [psid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TrainingNotFoundError extends NotFoundError {
|
class TrainingNotFoundError extends NotFoundError {
|
||||||
constructor(tid) {
|
constructor(tid) {
|
||||||
super('TrainingNotFoundError');
|
super('TrainingNotFoundError');
|
||||||
this.params = [tid];
|
this.params = [tid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContestNotFoundError extends NotFoundError {
|
class ContestNotFoundError extends NotFoundError {
|
||||||
constructor(cid) {
|
constructor(cid) {
|
||||||
super('ContestNotFoundError');
|
super('ContestNotFoundError');
|
||||||
this.params = [cid];
|
this.params = [cid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProblemDataNotFoundError extends NotFoundError {
|
class ProblemDataNotFoundError extends NotFoundError {
|
||||||
constructor(pid) {
|
constructor(pid) {
|
||||||
super('Data of problem {0} not found.');
|
super('Data of problem {0} not found.');
|
||||||
@@ -165,8 +204,30 @@ class ProblemDataNotFoundError extends NotFoundError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DiscussionNodeNotFoundError extends NotFoundError {
|
||||||
|
constructor(type, docId) {
|
||||||
|
super('Discussion node {0}/{1} not found.');
|
||||||
|
this.params = [type, docId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DocumentNotFoundError extends NotFoundError {
|
||||||
|
constructor(docId) {
|
||||||
|
super('Document {0} not found.');
|
||||||
|
this.params = [docId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiscussionNotFoundError extends NotFoundError {
|
||||||
|
constructor(did) {
|
||||||
|
super('Discussion {0} not found.');
|
||||||
|
this.params = [did];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
|
BlacklistedError,
|
||||||
ForbiddenError,
|
ForbiddenError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
LoginError,
|
LoginError,
|
||||||
@@ -193,49 +254,13 @@ module.exports = {
|
|||||||
SystemError,
|
SystemError,
|
||||||
TrainingAlreadyEnrollError,
|
TrainingAlreadyEnrollError,
|
||||||
RemoteOnlineJudgeError,
|
RemoteOnlineJudgeError,
|
||||||
|
DiscussionNodeNotFoundError,
|
||||||
|
DocumentNotFoundError,
|
||||||
|
DiscussionNotFoundError,
|
||||||
|
RoleAlreadyExistError,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
from vj4.model import builtin
|
|
||||||
|
|
||||||
|
|
||||||
class Error(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HashError(Error):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidStateError(Error):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UserFacingError(Error):
|
|
||||||
"""Error which faces end user."""
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {'name': self.__class__.__name__, 'args': self.args}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def http_status(self):
|
|
||||||
return 500
|
|
||||||
|
|
||||||
@property
|
|
||||||
def template_name(self):
|
|
||||||
return 'error.html'
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return 'An error has occurred.'
|
|
||||||
|
|
||||||
|
|
||||||
class BlacklistedError(ForbiddenError):
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return 'Address {0} is blacklisted.'
|
|
||||||
|
|
||||||
|
|
||||||
class FileTooLongError(ValidationError):
|
class FileTooLongError(ValidationError):
|
||||||
@property
|
@property
|
||||||
def message(self):
|
def message(self):
|
||||||
@@ -253,13 +278,6 @@ class UnknownFieldError(ForbiddenError):
|
|||||||
def message(self):
|
def message(self):
|
||||||
return 'Unknown field {0}.'
|
return 'Unknown field {0}.'
|
||||||
|
|
||||||
|
|
||||||
class DocumentNotFoundError(NotFoundError):
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return 'Document {2} not found.'
|
|
||||||
|
|
||||||
|
|
||||||
class CsrfTokenError(ForbiddenError):
|
class CsrfTokenError(ForbiddenError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -296,54 +314,12 @@ class DiscussionNodeAlreadyExistError(ForbiddenError):
|
|||||||
return 'Discussion node {1} already exists.'
|
return 'Discussion node {1} already exists.'
|
||||||
|
|
||||||
|
|
||||||
class DiscussionNodeNotFoundError(NotFoundError):
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return 'Discussion node {1} not found.'
|
|
||||||
|
|
||||||
|
|
||||||
class DiscussionNotFoundError(DocumentNotFoundError):
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return 'Discussion {1} not found.'
|
|
||||||
|
|
||||||
|
|
||||||
class MessageNotFoundError(NotFoundError):
|
class MessageNotFoundError(NotFoundError):
|
||||||
@property
|
@property
|
||||||
def message(self):
|
def message(self):
|
||||||
return 'Message {0} not found.'
|
return 'Message {0} not found.'
|
||||||
|
|
||||||
|
|
||||||
class InvalidJoinInvitationCodeError(ForbiddenError):
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return 'The invitation code you provided is invalid.'
|
|
||||||
|
|
||||||
|
|
||||||
class HomeworkScoreboardHiddenError(ForbiddenError):
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return 'Homework scoreboard is not visible.'
|
|
||||||
|
|
||||||
|
|
||||||
class HomeworkNotLiveError(ForbiddenError):
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return 'This homework is not open.'
|
|
||||||
|
|
||||||
|
|
||||||
class HomeworkAlreadyAttendedError(ForbiddenError):
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return "You've already claimed this homework."
|
|
||||||
|
|
||||||
|
|
||||||
class HomeworkNotAttendedError(ForbiddenError):
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return "You haven't claimed this homework yet."
|
|
||||||
|
|
||||||
|
|
||||||
class TrainingRequirementNotSatisfiedError(ForbiddenError):
|
class TrainingRequirementNotSatisfiedError(ForbiddenError):
|
||||||
@property
|
@property
|
||||||
def message(self):
|
def message(self):
|
||||||
@@ -355,11 +331,6 @@ class UsageExceededError(ForbiddenError):
|
|||||||
def message(self):
|
def message(self):
|
||||||
return 'Usage exceeded.'
|
return 'Usage exceeded.'
|
||||||
|
|
||||||
class ModifyBuiltinRoleError(ForbiddenError):
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return 'Built-in roles cannot be modified.'
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidArgumentError(BadRequestError):
|
class InvalidArgumentError(BadRequestError):
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
const paginate = require('../lib/paginate');
|
||||||
|
const problem = require('../model/problem');
|
||||||
|
const contest = require('../model/contest');
|
||||||
|
const user = require('../model/user');
|
||||||
|
const discussion = require('../model/discussion');
|
||||||
|
const {
|
||||||
|
Route, Handler,
|
||||||
|
} = require('../service/server');
|
||||||
|
const {
|
||||||
|
DiscussionNodeNotFoundError, DiscussionNotFoundError, DocumentNotFoundError,
|
||||||
|
} = require('../error');
|
||||||
|
const { constants } = require('../options');
|
||||||
|
const {
|
||||||
|
PERM_VIEW_DISCUSSION, PERM_EDIT_DISCUSSION, PERM_EDIT_DISCUSSION_REPLY,
|
||||||
|
PERM_VIEW_PROBLEM_HIDDEN, PERM_DELETE_DISCUSSION, PERM_DELETE_DISCUSSION_REPLY,
|
||||||
|
PERM_HIGHLIGHT_DISCUSSION, PERM_LOGGEDIN, PERM_CREATE_DISCUSSION,
|
||||||
|
PERM_REPLY_DISCUSSION,
|
||||||
|
} = require('../permission');
|
||||||
|
|
||||||
|
class DiscussionHandler extends Handler {
|
||||||
|
async _prepare({
|
||||||
|
type, docId, did, drid, drrid,
|
||||||
|
}) {
|
||||||
|
this.checkPerm(PERM_VIEW_DISCUSSION);
|
||||||
|
if (did) {
|
||||||
|
this.ddoc = await discussion.get(did);
|
||||||
|
if (!this.ddoc) throw new DiscussionNotFoundError(did);
|
||||||
|
type = this.ddoc.type;
|
||||||
|
docId = this.ddoc.parent;
|
||||||
|
if (drid) {
|
||||||
|
this.drdoc = await discussion.getReply(drid, did);
|
||||||
|
if (!this.drdoc) throw new DiscussionNotFoundError(drid);
|
||||||
|
if (this.drdoc.parent !== this.ddoc._id) throw new DocumentNotFoundError(drid);
|
||||||
|
if (drrid) {
|
||||||
|
[, this.drrdoc] = await discussion.getTailReply(drid, drrid);
|
||||||
|
if (!this.drrdoc) throw new DiscussionNotFoundError(drrid);
|
||||||
|
if (this.drrdoc.parent !== this.drdoc._id) throw new DocumentNotFoundError(drid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO(twd2): do more visibility check eg. contest
|
||||||
|
// TODO(twd2): exclude problem/contest discussions?
|
||||||
|
// TODO(iceboy): continuation based pagination.
|
||||||
|
if (type && docId) {
|
||||||
|
if (type === 'problem') {
|
||||||
|
const pdoc = await problem.getById(docId);
|
||||||
|
if (!pdoc) throw new DiscussionNodeNotFoundError(type, docId);
|
||||||
|
if (pdoc.hidden) this.checkPerm(PERM_VIEW_PROBLEM_HIDDEN);
|
||||||
|
this.title = pdoc.title;
|
||||||
|
} else if (type === 'contest') {
|
||||||
|
const tdoc = await contest.get(docId);
|
||||||
|
if (!tdoc) throw new DiscussionNodeNotFoundError(type, docId);
|
||||||
|
this.title = tdoc.title;
|
||||||
|
} else throw new DiscussionNodeNotFoundError(type, docId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiscussionMainHandler extends DiscussionHandler {
|
||||||
|
async get({ page = 1 }) {
|
||||||
|
const [ddocs, dpcount] = await paginate(discussion.getMulti(), page, constants.DISCUSSION_PER_PAGE);
|
||||||
|
const udict = await user.getList(ddocs.map((ddoc) => ddoc.owner));
|
||||||
|
this.response.template = 'discussion_main_or_node.html';
|
||||||
|
this.response.body = {
|
||||||
|
ddocs, dpcount, udict, page,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiscussionNodeHandler extends DiscussionHandler {
|
||||||
|
async get({ type, docId, page = 1 }) {
|
||||||
|
const [ddocs, dpcount] = await paginate(
|
||||||
|
discussion.getMulti({ type, docId }),
|
||||||
|
page,
|
||||||
|
constants.DISCUSSION_PER_PAGE,
|
||||||
|
);
|
||||||
|
const udict = await user.getList(ddocs.map((ddoc) => ddoc.owner));
|
||||||
|
const path = [
|
||||||
|
['discussion_main', '/discuss'],
|
||||||
|
[this.title, null, true],
|
||||||
|
];
|
||||||
|
this.response.template = 'discussion_main_or_node.html';
|
||||||
|
this.response.body = {
|
||||||
|
ddocs, dpcount, udict, path, page,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiscussionCreateHandler extends DiscussionHandler {
|
||||||
|
async prepare() {
|
||||||
|
this.checkPerm(PERM_LOGGEDIN);
|
||||||
|
this.checkPerm(PERM_CREATE_DISCUSSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get({ type, docId }) {
|
||||||
|
const path = [
|
||||||
|
['discussion_main', '/discuss'],
|
||||||
|
[this.title, `/discuss/${type}/${docId}`, true],
|
||||||
|
['discussion_create', null],
|
||||||
|
];
|
||||||
|
this.response.template = 'discussion_create.html';
|
||||||
|
this.response.body = { path, title: this.title };
|
||||||
|
}
|
||||||
|
|
||||||
|
async post({
|
||||||
|
type, docId, title, content, highlight,
|
||||||
|
}) {
|
||||||
|
this.limitRate('add_discussion', 3600, 30);
|
||||||
|
const flags = {};
|
||||||
|
if (highlight) {
|
||||||
|
this.checkPerm(PERM_HIGHLIGHT_DISCUSSION);
|
||||||
|
flags.highlight = true;
|
||||||
|
}
|
||||||
|
const did = await discussion.add(type, docId, this.user._id, title, content, this.request.ip, flags);
|
||||||
|
this.response.body = { did };
|
||||||
|
this.response.redirect = `/discuss/${did}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiscussionDetailHandler extends DiscussionHandler {
|
||||||
|
async get({ did, page = 1 }) {
|
||||||
|
const dsdoc = this.user.hasPerm(PERM_LOGGEDIN)
|
||||||
|
? await discussion.getStatus(did, this.user._id)
|
||||||
|
: null;
|
||||||
|
const [drdocs, pcount, drcount] = await paginate(
|
||||||
|
discussion.getMultiReply(did),
|
||||||
|
page,
|
||||||
|
constants.REPLY_PER_PAGE,
|
||||||
|
);
|
||||||
|
const uids = drdocs.map((drdoc) => drdoc.owner);
|
||||||
|
uids.push(this.ddoc.owner);
|
||||||
|
for (const drdoc of drdocs) {
|
||||||
|
if (drdoc.reply) {
|
||||||
|
for (const drrdoc of drdocs) {
|
||||||
|
uids.push(drrdoc.owner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const udict = await user.getList(uids);
|
||||||
|
const path = [
|
||||||
|
['discussion_main', '/discuss'],
|
||||||
|
[this.title, '/discuss/{type}', true],
|
||||||
|
[this.ddoc.title, null, true],
|
||||||
|
];
|
||||||
|
this.response.template = 'discussion_detail.html';
|
||||||
|
this.response.body = {
|
||||||
|
path, ddoc: this.ddoc, dsdoc, drdocs, page, pcount, drcount, udict,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async postReply({ did, content }) {
|
||||||
|
this.checkPerm(PERM_LOGGEDIN);
|
||||||
|
this.checkPerm(PERM_REPLY_DISCUSSION);
|
||||||
|
this.limitRate('add_discussion', 3600, 30);
|
||||||
|
await discussion.addReply(did, this.user._id, content, this.request.ip);
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async postTailReply({ drid, content }) {
|
||||||
|
this.checkPerm(PERM_LOGGEDIN);
|
||||||
|
this.checkPerm(PERM_REPLY_DISCUSSION);
|
||||||
|
this.limitRate('add_discussion', 3600, 30);
|
||||||
|
await discussion.addTailReply(drid, this.user._id, content, this.request.ip);
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async postEditReply({ drid, content }) {
|
||||||
|
if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_DISCUSSION_REPLY);
|
||||||
|
await discussion.editReply(drid, content);
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async postDeleteReply({ drid }) {
|
||||||
|
if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM_DELETE_DISCUSSION_REPLY);
|
||||||
|
await discussion.deleteReply(drid);
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async postEditTailReply({ drid, drrid, content }) {
|
||||||
|
if (this.drdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_DISCUSSION_REPLY);
|
||||||
|
await discussion.editTailReply(drid, drrid, content);
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async postDeleteTailReply({ drid, drrid }) {
|
||||||
|
if (this.drrdoc.owner !== this.user._id) this.checkPerm(PERM_DELETE_DISCUSSION_REPLY);
|
||||||
|
await discussion.deleteTailReply(drid, drrid);
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async postStar({ did, star }) {
|
||||||
|
await discussion.setStar(did, this.user._id, true);
|
||||||
|
this.response.body = { star };
|
||||||
|
this.response.direct = this.request.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async postUnstar({ did, star }) {
|
||||||
|
await discussion.setStar(did, this.user._id, false);
|
||||||
|
this.response.body = { star };
|
||||||
|
this.response.direct = this.request.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiscussionDetailRawHandler extends DiscussionHandler {
|
||||||
|
async get() {
|
||||||
|
this.response.type = 'text/markdown';
|
||||||
|
this.response.body = this.ddoc.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiscussionReplyRawHandler extends DiscussionHandler {
|
||||||
|
async get() {
|
||||||
|
this.response.type = 'text/markdown';
|
||||||
|
this.response.body = this.drdoc.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DiscussionTailReplyRawHandler extends DiscussionHandler {
|
||||||
|
async get() {
|
||||||
|
this.response.type = 'text/markdown';
|
||||||
|
this.response.body = this.drrdoc.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiscussionEditHandler extends DiscussionHandler {
|
||||||
|
async get() {
|
||||||
|
if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_DISCUSSION);
|
||||||
|
this.response.template = 'discussion_edit.html';
|
||||||
|
this.response.body = { ddoc: this.ddoc };
|
||||||
|
}
|
||||||
|
|
||||||
|
async post({
|
||||||
|
did, title, content, highlight, operation,
|
||||||
|
}) {
|
||||||
|
if (operation) return;
|
||||||
|
if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_DISCUSSION);
|
||||||
|
if (highlight && !this.ddoc.highlight) this.checkPerm(PERM_HIGHLIGHT_DISCUSSION);
|
||||||
|
await discussion.edit(did, title, content, highlight);
|
||||||
|
this.response.body = { did };
|
||||||
|
this.response.redirect = `/discuss/${did}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async postDelete({ did }) {
|
||||||
|
if (this.ddoc.owner !== this.user._id) this.checkPerm(PERM_DELETE_DISCUSSION);
|
||||||
|
await discussion.delete(did);
|
||||||
|
this.response.body = { type: this.ddoc.type, parent: this.ddoc.parent };
|
||||||
|
this.response.redirect = `/discuss/${this.ddoc.type}/${this.ddoc.parent}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Route('/discuss', DiscussionMainHandler);
|
||||||
|
Route('/discuss/:did', DiscussionDetailHandler);
|
||||||
|
Route('/discuss/:did/edit', DiscussionEditHandler);
|
||||||
|
Route('/discuss/:did/raw', DiscussionDetailRawHandler);
|
||||||
|
Route('/discuss/:did/:drid/raw', DiscussionReplyRawHandler);
|
||||||
|
Route('/discuss/:did/:drid/:drrid/raw', DiscussionTailReplyRawHandler);
|
||||||
|
Route('/discuss/:type/:docId', DiscussionNodeHandler);
|
||||||
|
Route('/discuss/:type/:docId/create', DiscussionCreateHandler);
|
||||||
@@ -8,7 +8,7 @@ const { ValidationError, RemoteOnlineJudgeError } = require('../error');
|
|||||||
|
|
||||||
class ProblemImportHandler extends Handler {
|
class ProblemImportHandler extends Handler {
|
||||||
async syzoj(url) {
|
async syzoj(url) {
|
||||||
const RE_SYZOJ = /https?:\/\/([a-zA-Z0-9\.]+)\/problem\/([0-9]+)\/?/i;
|
const RE_SYZOJ = /https?:\/\/([a-zA-Z0-9.]+)\/problem\/([0-9]+)\/?/i;
|
||||||
assert(url.match(RE_SYZOJ), new ValidationError('url'));
|
assert(url.match(RE_SYZOJ), new ValidationError('url'));
|
||||||
if (!url.endsWith('/')) url += '/';
|
if (!url.endsWith('/')) url += '/';
|
||||||
const [, host, pid] = RE_SYZOJ.exec(url);
|
const [, host, pid] = RE_SYZOJ.exec(url);
|
||||||
|
|||||||
+46
-20
@@ -96,16 +96,29 @@ class JudgeHandler extends Handler {
|
|||||||
let rid = await queue.get('judge', false);
|
let rid = await queue.get('judge', false);
|
||||||
while (rid) {
|
while (rid) {
|
||||||
const rdoc = await record.get(rid); // eslint-disable-line no-await-in-loop
|
const rdoc = await record.get(rid); // eslint-disable-line no-await-in-loop
|
||||||
const pdoc = await problem.getById(rdoc.pid); // eslint-disable-line no-await-in-loop
|
let task;
|
||||||
const task = {
|
if (!rdoc.input) {
|
||||||
event: 'judge',
|
// eslint-disable-next-line no-await-in-loop
|
||||||
rid,
|
const pdoc = await problem.getById(rdoc.pid);
|
||||||
type: 0,
|
task = {
|
||||||
pid: rdoc.pid,
|
event: 'judge',
|
||||||
data: pdoc.data,
|
rid,
|
||||||
lang: rdoc.lang,
|
type: 0,
|
||||||
code: rdoc.code,
|
pid: rdoc.pid,
|
||||||
};
|
data: pdoc.data,
|
||||||
|
lang: rdoc.lang,
|
||||||
|
code: rdoc.code,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
task = {
|
||||||
|
event: 'pretest',
|
||||||
|
rid,
|
||||||
|
type: 0,
|
||||||
|
input: rdoc.input,
|
||||||
|
lang: rdoc.lang,
|
||||||
|
code: rdoc.code,
|
||||||
|
};
|
||||||
|
}
|
||||||
tasks.push(task);
|
tasks.push(task);
|
||||||
rid = await queue.get('judge', false); // eslint-disable-line no-await-in-loop
|
rid = await queue.get('judge', false); // eslint-disable-line no-await-in-loop
|
||||||
}
|
}
|
||||||
@@ -134,16 +147,29 @@ class JudgeConnectionHandler extends ConnectionHandler {
|
|||||||
this.processing = null;
|
this.processing = null;
|
||||||
const rid = await queue.get('judge');
|
const rid = await queue.get('judge');
|
||||||
const rdoc = await record.get(rid);
|
const rdoc = await record.get(rid);
|
||||||
const pdoc = await problem.getById(rdoc.pid);
|
let task;
|
||||||
const task = {
|
if (!rdoc.input) {
|
||||||
event: 'judge',
|
// eslint-disable-next-line no-await-in-loop
|
||||||
rid,
|
const pdoc = await problem.getById(rdoc.pid);
|
||||||
type: 0,
|
task = {
|
||||||
pid: rdoc.pid,
|
event: 'judge',
|
||||||
data: pdoc.data,
|
rid,
|
||||||
lang: rdoc.lang,
|
type: 0,
|
||||||
code: rdoc.code,
|
pid: rdoc.pid,
|
||||||
};
|
data: pdoc.data,
|
||||||
|
lang: rdoc.lang,
|
||||||
|
code: rdoc.code,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
task = {
|
||||||
|
event: 'pretest',
|
||||||
|
rid,
|
||||||
|
type: 0,
|
||||||
|
input: rdoc.input,
|
||||||
|
lang: rdoc.lang,
|
||||||
|
code: rdoc.code,
|
||||||
|
};
|
||||||
|
}
|
||||||
this.send({ task });
|
this.send({ task });
|
||||||
this.processing = task.rid;
|
this.processing = task.rid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
const system = require('../model/system');
|
||||||
|
const user = require('../model/user');
|
||||||
|
const builtin = require('../model/builtin');
|
||||||
|
const { Route, Handler } = require('../service/server');
|
||||||
|
const { PERM_ADMIN } = require('../permission');
|
||||||
|
const { RoleAlreadyExistError } = require('../error');
|
||||||
|
|
||||||
|
class ManageHandler extends Handler {
|
||||||
|
async prepare() {
|
||||||
|
this.checkPerm(PERM_ADMIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ManageMainHandler extends ManageHandler {
|
||||||
|
async get() {
|
||||||
|
this.response.redirect = '/manage/dashboard';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ManageDashboardHandler extends ManageHandler {
|
||||||
|
async get() {
|
||||||
|
this.response.template = 'domain_manage_dashboard.html';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ManageEditHandler extends ManageHandler {
|
||||||
|
async get() {
|
||||||
|
this.response.template = 'domain_manage_edit.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
async post({ name, gravatar, bulletin }) {
|
||||||
|
await Promise.all([
|
||||||
|
system.set('name', name),
|
||||||
|
system.set('gravatar', gravatar),
|
||||||
|
system.set('bulletin', bulletin),
|
||||||
|
]);
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ManageUserHandler extends ManageHandler {
|
||||||
|
async get() {
|
||||||
|
const uids = [];
|
||||||
|
const [rudocs, udocs, roles] = await Promise.all([
|
||||||
|
system.get(),
|
||||||
|
user.getMulti({ role: { $neq: 'default' } }).toArray(),
|
||||||
|
user.getRoles(),
|
||||||
|
]);
|
||||||
|
for (const udoc of udocs) {
|
||||||
|
uids.push(udoc._id);
|
||||||
|
rudocs[udoc.role].push(udoc);
|
||||||
|
}
|
||||||
|
const roles_with_text = roles.map((role) => [role, role]);
|
||||||
|
const udict = await user.getList(uids);
|
||||||
|
this.response.template = 'domain_manage_user.html';
|
||||||
|
this.response.body = {
|
||||||
|
roles, roles_with_text, rudocs, udict,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async postSetUser({ uid, role }) {
|
||||||
|
await user.setById(uid, { role });
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ManagePermissionHandler extends ManageHandler {
|
||||||
|
async get() {
|
||||||
|
const roles = await user.getRoles();
|
||||||
|
this.response.template = 'domain_manage_permission.html';
|
||||||
|
this.response.body = { roles };
|
||||||
|
}
|
||||||
|
|
||||||
|
async post({ roles }) {
|
||||||
|
for (const role of roles) {
|
||||||
|
let perms = '';
|
||||||
|
for (const perm of roles) {
|
||||||
|
perms += perm;
|
||||||
|
}
|
||||||
|
roles[role] = perms;
|
||||||
|
}
|
||||||
|
await user.setRoles(roles);
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ManageRoleHandler extends ManageHandler {
|
||||||
|
async get() {
|
||||||
|
const roles = await user.getRoles();
|
||||||
|
this.response.template = 'domain_manage_role.html';
|
||||||
|
this.response.body = { roles };
|
||||||
|
}
|
||||||
|
|
||||||
|
async postAdd({ role }) {
|
||||||
|
const roles = await user.getRoles();
|
||||||
|
if (roles.includes(role)) throw new RoleAlreadyExistError(role);
|
||||||
|
await user.setRole(role, builtin.PERM_DEFAULT);
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
async postDelete({ role }) {
|
||||||
|
await user.deleteRoles(role);
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Route('/manage', ManageMainHandler);
|
||||||
|
Route('/manage/dashboard', ManageDashboardHandler);
|
||||||
|
Route('/manage/edit', ManageEditHandler);
|
||||||
|
Route('/manage/user', ManageUserHandler);
|
||||||
|
Route('/manage/permission', ManagePermissionHandler);
|
||||||
|
Route('/manage/role', ManageRoleHandler);
|
||||||
+50
-98
@@ -6,7 +6,10 @@ const record = require('../model/record');
|
|||||||
const user = require('../model/user');
|
const user = require('../model/user');
|
||||||
const solution = require('../model/solution');
|
const solution = require('../model/solution');
|
||||||
const system = require('../model/system');
|
const system = require('../model/system');
|
||||||
const { Route, Handler } = require('../service/server');
|
const bus = require('../service/bus');
|
||||||
|
const {
|
||||||
|
Route, Connection, Handler, ConnectionHandler,
|
||||||
|
} = require('../service/server');
|
||||||
const gridfs = require('../service/gridfs');
|
const gridfs = require('../service/gridfs');
|
||||||
const queue = require('../service/queue');
|
const queue = require('../service/queue');
|
||||||
const {
|
const {
|
||||||
@@ -123,6 +126,48 @@ class ProblemSubmitHandler extends ProblemDetailHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ProblemPretestHandler extends ProblemDetailHandler {
|
||||||
|
async post({ lang, code, input }) {
|
||||||
|
this.limitRate('add_record', 60, 100);
|
||||||
|
const rid = await record.add({
|
||||||
|
uid: this.uid, lang, code, pid: this.pdoc._id, input,
|
||||||
|
});
|
||||||
|
await queue.push('judge', rid);
|
||||||
|
this.response.body = { rid };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProblemPretestConnectionHandler extends ConnectionHandler {
|
||||||
|
async prepare({ pid }) {
|
||||||
|
this.pid = pid.toString();
|
||||||
|
bus.subscribe(['record_change'], this.onRecordChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onRecordChange(data) {
|
||||||
|
const rdoc = data.value;
|
||||||
|
if (rdoc.uid !== this.user._id || rdoc.pid.toString() !== this.pid) return;
|
||||||
|
if (rdoc.tid) return;
|
||||||
|
this.send({ rdoc });
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup() {
|
||||||
|
bus.unsubscribe(['record_change'], this.onRecordChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProblemStatisticsHandler extends ProblemDetailHandler {
|
||||||
|
async get() {
|
||||||
|
const udoc = await user.getById(this.pdoc.owner);
|
||||||
|
const path = [
|
||||||
|
['problem_main', '/p'],
|
||||||
|
[this.pdoc.title, `/p/${this.pdoc.pid}`, true],
|
||||||
|
['problem_statistics', null],
|
||||||
|
];
|
||||||
|
this.response.template = 'problem_statistics.html';
|
||||||
|
this.response.body = { pdoc: this.pdoc, udoc, path };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -322,7 +367,7 @@ class ProblemCreateHandler extends Handler {
|
|||||||
title, pid, content, hidden,
|
title, pid, content, hidden,
|
||||||
}) {
|
}) {
|
||||||
validator.checkPid(pid);
|
validator.checkPid(pid);
|
||||||
pid = pid || await system.incPidCounter();
|
pid = pid || await system.inc('pid');
|
||||||
await problem.add({
|
await problem.add({
|
||||||
title, content, owner: this.user._id, pid, hidden,
|
title, content, owner: this.user._id, pid, hidden,
|
||||||
});
|
});
|
||||||
@@ -335,7 +380,9 @@ Route('/p', ProblemHandler);
|
|||||||
Route('/problem/random', ProblemRandomHandler);
|
Route('/problem/random', ProblemRandomHandler);
|
||||||
Route('/p/:pid', ProblemDetailHandler);
|
Route('/p/:pid', ProblemDetailHandler);
|
||||||
Route('/p/:pid/submit', ProblemSubmitHandler);
|
Route('/p/:pid/submit', ProblemSubmitHandler);
|
||||||
|
Route('/p/:pid/pretest', ProblemPretestHandler);
|
||||||
Route('/p/:pid/settings', ProblemSettingsHandler);
|
Route('/p/:pid/settings', ProblemSettingsHandler);
|
||||||
|
Route('/p/:pid/statistics', ProblemStatisticsHandler);
|
||||||
Route('/p/:pid/edit', ProblemEditHandler);
|
Route('/p/:pid/edit', ProblemEditHandler);
|
||||||
Route('/p/:pid/upload', ProblemDataUploadHandler);
|
Route('/p/:pid/upload', ProblemDataUploadHandler);
|
||||||
Route('/p/:pid/data', ProblemDataDownloadHandler);
|
Route('/p/:pid/data', ProblemDataDownloadHandler);
|
||||||
@@ -343,99 +390,4 @@ Route('/p/:pid/solution', ProblemSolutionHandler);
|
|||||||
Route('/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler);
|
Route('/p/:pid/solution/:psid/raw', ProblemSolutionRawHandler);
|
||||||
Route('/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionReplyRawHandler);
|
Route('/p/:pid/solution/:psid/:psrid/raw', ProblemSolutionReplyRawHandler);
|
||||||
Route('/problem/create', ProblemCreateHandler);
|
Route('/problem/create', ProblemCreateHandler);
|
||||||
|
Connection('/p/:pid/pretest-conn', ProblemPretestConnectionHandler);
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
@app.route('/p/{pid}/pretest', 'problem_pretest')
|
|
||||||
class ProblemPretestHandler(base.Handler):
|
|
||||||
@base.requirePerm(builtin.PERM_SUBMIT_PROBLEM)
|
|
||||||
@base.route_argument
|
|
||||||
@base.post_argument
|
|
||||||
@base.require_csrfToken
|
|
||||||
@base.sanitize
|
|
||||||
@base.limitRate('add_record', 60, 100)
|
|
||||||
async def post(self, *, pid: document.convert_doc_id, lang: str, code: str,
|
|
||||||
data_input: str, data_output: str):
|
|
||||||
pdoc = await problem.get(self.domainId, pid)
|
|
||||||
# don't need to check hidden status
|
|
||||||
# create zip file, TODO(twd2): check file size
|
|
||||||
post = await self.request.post()
|
|
||||||
content = list(zip(post.getall('data_input'), post.getall('data_output')))
|
|
||||||
output_buffer = io.BytesIO()
|
|
||||||
zip_file = zipfile.ZipFile(output_buffer, 'a', zipfile.ZIP_DEFLATED)
|
|
||||||
config_content = str(len(content)) + '\n'
|
|
||||||
for i, (data_input, data_output) in enumerate(content):
|
|
||||||
input_file = 'input{0}.txt'.format(i)
|
|
||||||
output_file = 'output{0}.txt'.format(i)
|
|
||||||
config_content += '{0}|{1}|1|10|262144\n'.format(input_file, output_file)
|
|
||||||
zip_file.writestr('Input/{0}'.format(input_file), data_input)
|
|
||||||
zip_file.writestr('Output/{0}'.format(output_file), data_output)
|
|
||||||
zip_file.writestr('Config.ini', config_content)
|
|
||||||
# mark all files as created in Windows :p
|
|
||||||
for zfile in zip_file.filelist:
|
|
||||||
zfile.create_system = 0
|
|
||||||
zip_file.close()
|
|
||||||
fid = await fs.add_data('application/zip', output_buffer.getvalue())
|
|
||||||
output_buffer.close()
|
|
||||||
rid = await record.add(self.domainId, pdoc['_id'], constant.record.TYPE_PRETEST,
|
|
||||||
self.user['_id'], lang, code, fid)
|
|
||||||
self.json_or_redirect(self.reverse_url('record_detail', rid=rid))
|
|
||||||
|
|
||||||
|
|
||||||
@app.connection_route('/p/{pid}/pretest-conn', 'problem_pretest-conn')
|
|
||||||
class ProblemPretestConnection(record_handler.RecordVisibilityMixin, base.Connection):
|
|
||||||
async def on_open(self):
|
|
||||||
await super(ProblemPretestConnection, self).on_open()
|
|
||||||
self.pid = document.convert_doc_id(self.request.match_info['pid'])
|
|
||||||
bus.subscribe(self.on_record_change, ['record_change'])
|
|
||||||
|
|
||||||
async def on_record_change(self, e):
|
|
||||||
rdoc = e['value']
|
|
||||||
if rdoc['uid'] != self.user['_id'] or \
|
|
||||||
rdoc['domainId'] != self.domainId or rdoc['pid'] != self.pid:
|
|
||||||
return
|
|
||||||
# check permission for visibility: contest
|
|
||||||
if rdoc['tid']:
|
|
||||||
show_status, tdoc = await self.rdoc_contest_visible(rdoc)
|
|
||||||
if not show_status:
|
|
||||||
return
|
|
||||||
self.send(rdoc=rdoc)
|
|
||||||
|
|
||||||
async def on_close(self):
|
|
||||||
bus.unsubscribe(self.on_record_change)
|
|
||||||
|
|
||||||
|
|
||||||
@base.requirePriv(builtin.PRIV_USER_PROFILE)
|
|
||||||
@base.route_argument
|
|
||||||
@base.post_argument
|
|
||||||
@base.require_csrfToken
|
|
||||||
@base.sanitize
|
|
||||||
async def post(self, *, pid: document.convert_doc_id, title: str, content: str):
|
|
||||||
pdoc = await problem.get(self.domainId, pid)
|
|
||||||
if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF):
|
|
||||||
self.check_perm(builtin.PERM_EDIT_PROBLEM)
|
|
||||||
await problem.edit(self.domainId, pdoc['_id'], title=title, content=content)
|
|
||||||
self.json_or_redirect(self.reverse_url('problem_detail', pid=pid))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/p/{pid}/statistics', 'problem_statistics')
|
|
||||||
class ProblemStatisticsHandler(base.Handler):
|
|
||||||
@base.route_argument
|
|
||||||
@base.sanitize
|
|
||||||
async def get(self, *, pid: document.convert_doc_id):
|
|
||||||
# TODO(twd2)
|
|
||||||
uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None
|
|
||||||
pdoc = await problem.get(self.domainId, pid, uid)
|
|
||||||
if pdoc.get('hidden', False):
|
|
||||||
self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN)
|
|
||||||
udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']),
|
|
||||||
domain.get_user(self.domainId, pdoc['owner_uid']))
|
|
||||||
path_components = self.build_path(
|
|
||||||
(self.translate('problem_main'), self.reverse_url('problem_main')),
|
|
||||||
(pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['_id'])),
|
|
||||||
(self.translate('problem_statistics'), None))
|
|
||||||
self.render('problem_statistics.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc,
|
|
||||||
page_title=pdoc['title'], path_components=path_components)
|
|
||||||
|
|
||||||
*/
|
|
||||||
@@ -96,6 +96,7 @@ class RecordConnectionHandler extends ConnectionHandler {
|
|||||||
async onRecordChange(data) {
|
async onRecordChange(data) {
|
||||||
const 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;
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
let [udoc, pdoc] = await Promise.all([user.getById(rdoc.uid), problem.getById(rdoc.pid)]);
|
let [udoc, pdoc] = await Promise.all([user.getById(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 }) });
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class UserRegisterWithCodeHandler extends Handler {
|
|||||||
const { 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);
|
||||||
if (password !== verifyPassword) throw new VerifyPasswordError();
|
if (password !== verifyPassword) throw new VerifyPasswordError();
|
||||||
const uid = await system.incUserCounter();
|
const uid = await system.inc('user');
|
||||||
await user.create({
|
await user.create({
|
||||||
uid, uname, password, mail, regip: this.request.ip,
|
uid, uname, password, mail, regip: this.request.ip,
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-9
@@ -1,7 +1,4 @@
|
|||||||
const fs = require('fs');
|
const locales = require('../../.build/locales.json');
|
||||||
const yaml = require('js-yaml');
|
|
||||||
|
|
||||||
const locales = {};
|
|
||||||
|
|
||||||
String.prototype.format = function formatStr(...args) {
|
String.prototype.format = function formatStr(...args) {
|
||||||
let result = this;
|
let result = this;
|
||||||
@@ -33,8 +30,4 @@ String.prototype.translate = function translate(language = 'zh_CN') {
|
|||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = function load(file, language) {
|
module.exports = locales;
|
||||||
if (!locales[language]) locales[language] = {};
|
|
||||||
const content = fs.readFileSync(file).toString();
|
|
||||||
Object.assign(locales[language], yaml.safeLoad(content));
|
|
||||||
};
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
/* eslint-disable no-bitwise */
|
/* eslint-disable no-bitwise */ /* eslint-disable max-len */
|
||||||
function safeAdd(x, y) {
|
function safeAdd(x, y) {
|
||||||
const lsw = (x & 0xffff) + (y & 0xffff);
|
const lsw = (x & 0xffff) + (y & 0xffff);
|
||||||
const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
const { ObjectID } = require('bson');
|
||||||
|
const { DocumentNotFoundError } = require('../error');
|
||||||
|
const db = require('../service/db.js');
|
||||||
|
|
||||||
|
const coll = db.collection('discussion');
|
||||||
|
const collReply = db.collection('discussion.reply');
|
||||||
|
const collStatus = db.collection('discussion.status');
|
||||||
|
|
||||||
|
function add(parentType, parentId, owner, title, content, ip = null, highlight = false) {
|
||||||
|
return coll.insertOne({
|
||||||
|
owner,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
ip,
|
||||||
|
highlight,
|
||||||
|
parentType,
|
||||||
|
parentId,
|
||||||
|
nReply: 0,
|
||||||
|
updateAt: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(did) {
|
||||||
|
return coll.findOne({ _id: did });
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(did, title, content, highlight) {
|
||||||
|
return coll.findOneAndUpdate({ _id: did }, { $set: { title, content, highlight } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function del(did) {
|
||||||
|
return Promise.all([
|
||||||
|
coll.deleteOne({ _id: did }),
|
||||||
|
collReply.deleteMany({ did }),
|
||||||
|
collStatus.deleteMany({ did }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function count(query) {
|
||||||
|
return coll.find(query).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMulti(query) {
|
||||||
|
return coll.find(query).sort([{ updateAt: -1 }, { _id: -1 }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addReply(did, owner, content, ip) {
|
||||||
|
const [drdoc] = await Promise.all([
|
||||||
|
collReply.insertOne({
|
||||||
|
owner, did, content, ip,
|
||||||
|
}),
|
||||||
|
coll.updateOne({ _id: did }, { $inc: { nReply: 1 }, $set: { updateAt: new Date() } }),
|
||||||
|
]);
|
||||||
|
return drdoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReply(drid) {
|
||||||
|
return collReply.findOne({ _id: drid });
|
||||||
|
}
|
||||||
|
|
||||||
|
function editReply(drid, content) {
|
||||||
|
return collReply.updateOne({ _id: drid }, { $set: { content } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function delReply(drid) {
|
||||||
|
const drdoc = await getReply(drid);
|
||||||
|
if (!drdoc) throw new DocumentNotFoundError(drid);
|
||||||
|
return await Promise.all([
|
||||||
|
collReply.deleteOne({ _id: drid }),
|
||||||
|
coll.updateOne({ _id: drdoc.did }, { $inc: { nReply: -1 } }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMultiReply(did) {
|
||||||
|
return coll.find({ did }).sort('_id', -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getListReply(did) {
|
||||||
|
return getMultiReply({ did }).toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addTailReply(drid, owner, content, ip) {
|
||||||
|
let drdoc = await collReply.findOne({ _id: drid });
|
||||||
|
const sid = new ObjectID();
|
||||||
|
await Promise.all([
|
||||||
|
collReply.updateOne({ _id: drid }, {
|
||||||
|
$push: {
|
||||||
|
reply: {
|
||||||
|
_id: sid, content, owner, ip,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
coll.updateOne({ _id: drdoc.did }, { $set: { updateAt: new Date() } }),
|
||||||
|
]);
|
||||||
|
drdoc = await collReply.findOne({ _id: drid });
|
||||||
|
return [drdoc, sid];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTailReply(drid, drrid) {
|
||||||
|
const drdoc = await collReply.findOne({ _id: drid, reply: { $elemMatch: { _id: drrid } } });
|
||||||
|
if (!drdoc) return [null, null];
|
||||||
|
for (const drrdoc of drdoc) if (drrdoc._id === drrid) return [drdoc, drrdoc];
|
||||||
|
return [drdoc, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editTailReply(drid, drrid, content) {
|
||||||
|
const drdoc = await collReply.findOne({ _id: drid, reply: { $elemMatch: { _id: drrid } } });
|
||||||
|
const { reply } = drdoc;
|
||||||
|
for (const i in reply) {
|
||||||
|
if (reply[i]._id === drrid) {
|
||||||
|
reply[i].content = content;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await collReply.findOneAndUpdate({ _id: drdoc._id }, { $set: { reply } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function delTailReply(drid, drrid) {
|
||||||
|
return coll.findOneAndUpdate({ _id: drid }, { $pull: { reply: { _id: drrid } } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStar(did, uid, star) {
|
||||||
|
return collStatus.findOneAndUpdate({ did, uid }, { $set: { star } }, { upsert: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatus(did, uid) {
|
||||||
|
return collStatus.findOne({ did, uid });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
add,
|
||||||
|
get,
|
||||||
|
edit,
|
||||||
|
del,
|
||||||
|
count,
|
||||||
|
getMulti,
|
||||||
|
addReply,
|
||||||
|
getReply,
|
||||||
|
editReply,
|
||||||
|
delReply,
|
||||||
|
getMultiReply,
|
||||||
|
getListReply,
|
||||||
|
addTailReply,
|
||||||
|
getTailReply,
|
||||||
|
editTailReply,
|
||||||
|
delTailReply,
|
||||||
|
setStar,
|
||||||
|
getStatus,
|
||||||
|
};
|
||||||
+14
-1
@@ -19,16 +19,19 @@ async function add(pid, owner, content) {
|
|||||||
});
|
});
|
||||||
return res.insertedId;
|
return res.insertedId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get(psid) {
|
async function get(psid) {
|
||||||
psid = new ObjectID(psid);
|
psid = new ObjectID(psid);
|
||||||
const 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMany(query, sort, page, limit) {
|
function getMany(query, sort, page, limit) {
|
||||||
return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit)
|
return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit)
|
||||||
.toArray();
|
.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 } });
|
||||||
@@ -36,15 +39,19 @@ async function edit(_id, content) {
|
|||||||
if (!psdoc) throw new SolutionNotFoundError(_id);
|
if (!psdoc) throw new SolutionNotFoundError(_id);
|
||||||
return psdoc;
|
return psdoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
function del(psid) {
|
function del(psid) {
|
||||||
return coll.deleteOne({ _id: psid });
|
return coll.deleteOne({ _id: psid });
|
||||||
}
|
}
|
||||||
|
|
||||||
function count(query) {
|
function count(query) {
|
||||||
return 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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function reply(psid, owner, content) {
|
function reply(psid, owner, content) {
|
||||||
validator.checkContent(content);
|
validator.checkContent(content);
|
||||||
return coll.findOneAndUpdate(
|
return coll.findOneAndUpdate(
|
||||||
@@ -52,18 +59,20 @@ function reply(psid, owner, content) {
|
|||||||
{ $push: { reply: { content, owner, _id: new ObjectID() } } },
|
{ $push: { reply: { content, owner, _id: new ObjectID() } } },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getReply(psid, psrid) {
|
async function getReply(psid, psrid) {
|
||||||
const psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } });
|
const psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } });
|
||||||
if (!psdoc) return [null, null];
|
if (!psdoc) return [null, null];
|
||||||
for (const psrdoc of psdoc) if (psrdoc._id === psrid) return [psdoc, psrdoc];
|
for (const psrdoc of psdoc) 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);
|
||||||
const psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } });
|
const psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } });
|
||||||
const { reply } = psdoc;
|
const { reply } = psdoc; // eslint-disable-line no-shadow
|
||||||
for (const 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;
|
||||||
@@ -72,9 +81,11 @@ async function editReply(psid, psrid, content) {
|
|||||||
}
|
}
|
||||||
return await coll.findOneAndUpdate({ _id: psdoc._id }, { $set: { reply } });
|
return await coll.findOneAndUpdate({ _id: psdoc._id }, { $set: { reply } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function delReply(psid, psrid) {
|
function delReply(psid, psrid) {
|
||||||
return 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 collStatus.findOne({ psid, uid });
|
let pssdoc = await collStatus.findOne({ psid, uid });
|
||||||
if (pssdoc) await collStatus.deleteOne({ psid, uid });
|
if (pssdoc) await collStatus.deleteOne({ psid, uid });
|
||||||
@@ -85,12 +96,14 @@ async function vote(psid, uid, value) {
|
|||||||
const 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) {
|
||||||
const result = {};
|
const result = {};
|
||||||
const res = await collStatus.find({ uid, psid: { $in: list } }).toArray();
|
const res = await collStatus.find({ uid, psid: { $in: list } }).toArray();
|
||||||
for (const i of res) result[i.psid] = i;
|
for (const i of res) result[i.psid] = i;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
count,
|
count,
|
||||||
add,
|
add,
|
||||||
|
|||||||
+9
-30
@@ -11,41 +11,20 @@ async function update(_id, operation, config) {
|
|||||||
await coll.findOneAndUpdate({ _id }, operation, config);
|
await coll.findOneAndUpdate({ _id }, operation, config);
|
||||||
return get(_id);
|
return get(_id);
|
||||||
}
|
}
|
||||||
|
async function set(_id, value) {
|
||||||
|
await coll.findOneAndUpdate({ _id }, { value }, { upsert: true });
|
||||||
|
return get(_id);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Increments the user counter.
|
* Increments the counter.
|
||||||
* @returns {number} Integer value after increment.
|
* @returns {number} Integer value after increment.
|
||||||
*/
|
*/
|
||||||
function incUserCounter() {
|
function inc(field) {
|
||||||
return update('userCounter', { $inc: { value: 1 } }, { upsert: true });
|
return update(field, { $inc: { value: 1 } }, { upsert: true });
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Increments the problem ID counter.
|
|
||||||
* @returns {number} Integer value before increment.
|
|
||||||
*/
|
|
||||||
function incPidCounter() {
|
|
||||||
return update('userCounter', { $inc: { value: 1 } }, { upsert: true });
|
|
||||||
}
|
|
||||||
async function acquireLock(name) {
|
|
||||||
const value = Math.floor(Math.random() * 0xFFFFFFFF);
|
|
||||||
try {
|
|
||||||
await coll.updateOne({ _id: `lock_${name}`, value: 0 }, { $set: { value } }, { upsert: true });
|
|
||||||
} catch (e) { return null; }
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
async function releaseLock(name, value) {
|
|
||||||
const result = await coll.updateOne({ _id: `lock_${name}`, value }, { $set: { value: 0 } });
|
|
||||||
return !!result.matchedCount;
|
|
||||||
}
|
|
||||||
async function releaseLockAnyway(name) {
|
|
||||||
await coll.updateOne({ _id: `lock_${name}` }, { $set: { value: 0 } });
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
module.exports = {
|
module.exports = {
|
||||||
get,
|
get,
|
||||||
update,
|
update,
|
||||||
incPidCounter,
|
inc,
|
||||||
incUserCounter,
|
set,
|
||||||
acquireLock,
|
|
||||||
releaseLock,
|
|
||||||
releaseLockAnyway,
|
|
||||||
};
|
};
|
||||||
@@ -9,15 +9,18 @@ const collStatus = db.collection('training.status');
|
|||||||
function getStatus(tid, uid) {
|
function getStatus(tid, uid) {
|
||||||
return collStatus.findOne({ tid, uid });
|
return collStatus.findOne({ tid, uid });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMultiStatus(query) {
|
function getMultiStatus(query) {
|
||||||
return collStatus.find(query);
|
return collStatus.find(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getListStatus(uid, tids) {
|
async function getListStatus(uid, tids) {
|
||||||
const tsdocs = await getMultiStatus({ uid, tid: { $in: Array.from(new Set(tids)) } }).toArray();
|
const tsdocs = await getMultiStatus({ uid, tid: { $in: Array.from(new Set(tids)) } }).toArray();
|
||||||
const r = {};
|
const r = {};
|
||||||
for (const tsdoc of tsdocs) r[tsdoc.pid] = tsdoc;
|
for (const tsdoc of tsdocs) r[tsdoc.pid] = tsdoc;
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enroll(tid, uid) {
|
async function enroll(tid, uid) {
|
||||||
try {
|
try {
|
||||||
await collStatus.insertOne({ tid, uid, enroll: 1 });
|
await collStatus.insertOne({ tid, uid, enroll: 1 });
|
||||||
@@ -26,6 +29,7 @@ async function enroll(tid, uid) {
|
|||||||
}
|
}
|
||||||
await coll.findOneAndUpdate({ _id: tid }, { $inc: { enroll: 1 } });
|
await coll.findOneAndUpdate({ _id: tid }, { $inc: { enroll: 1 } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setStatus(tid, uid, $set) {
|
async function setStatus(tid, uid, $set) {
|
||||||
await collStatus.findOneAndUpdate({ tid, uid }, { $set });
|
await collStatus.findOneAndUpdate({ tid, uid }, { $set });
|
||||||
return await collStatus.findOne({ tid, uid });
|
return await collStatus.findOne({ tid, uid });
|
||||||
|
|||||||
+7
-2
@@ -83,7 +83,7 @@ async function changePassword(uid, currentPassword, newPassword) {
|
|||||||
const udoc = await getById(uid);
|
const udoc = await getById(uid);
|
||||||
udoc.checkPassword(currentPassword);
|
udoc.checkPassword(currentPassword);
|
||||||
const salt = pwhash.salt();
|
const salt = pwhash.salt();
|
||||||
return await coll.findOneAndUpdate({
|
return await coll.findOneAndUpdate({ // eslint-disable-line no-return-await
|
||||||
_id: udoc._id,
|
_id: udoc._id,
|
||||||
}, {
|
}, {
|
||||||
$set: { salt, hash: pwhash.hash(newPassword, salt) },
|
$set: { salt, hash: pwhash.hash(newPassword, salt) },
|
||||||
@@ -101,7 +101,7 @@ async function create({
|
|||||||
validator.checkPassword(password);
|
validator.checkPassword(password);
|
||||||
validator.checkEmail(mail);
|
validator.checkEmail(mail);
|
||||||
const salt = pwhash.salt();
|
const salt = pwhash.salt();
|
||||||
if (!uid) uid = system.incUserCounter();
|
if (!uid) uid = system.inc('user');
|
||||||
try {
|
try {
|
||||||
await coll.insertOne({
|
await coll.insertOne({
|
||||||
_id: uid,
|
_id: uid,
|
||||||
@@ -127,6 +127,10 @@ function getMany(params) {
|
|||||||
return coll.find(params);
|
return coll.find(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRoles() {
|
||||||
|
return collRole.find().toArray();
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
changePassword,
|
changePassword,
|
||||||
create,
|
create,
|
||||||
@@ -139,4 +143,5 @@ module.exports = {
|
|||||||
setEmail,
|
setEmail,
|
||||||
setPassword,
|
setPassword,
|
||||||
getList,
|
getList,
|
||||||
|
getRoles,
|
||||||
};
|
};
|
||||||
@@ -58,4 +58,5 @@ try {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Cannot load config');
|
console.error('Cannot load config');
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = options;
|
module.exports = options;
|
||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
const Mongo = require('mongodb');
|
const Mongo = require('mongodb');
|
||||||
|
const bus = require('../service/bus');
|
||||||
const options = require('../options');
|
const options = require('../options');
|
||||||
|
|
||||||
let mongourl = 'mongodb://';
|
let mongourl = 'mongodb://';
|
||||||
@@ -12,7 +13,7 @@ Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology:
|
|||||||
.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');
|
bus.publish('system_database_connected', null);
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
+12
-4
@@ -17,7 +17,7 @@ const token = require('../model/token');
|
|||||||
const opcount = require('../model/opcount');
|
const opcount = require('../model/opcount');
|
||||||
const {
|
const {
|
||||||
UserNotFoundError, BlacklistedError, PermissionError,
|
UserNotFoundError, BlacklistedError, PermissionError,
|
||||||
NotFoundError, UserFacingError,
|
NotFoundError, UserFacingError, ValidationError,
|
||||||
} = require('../error');
|
} = require('../error');
|
||||||
|
|
||||||
const app = new Koa();
|
const app = new Koa();
|
||||||
@@ -111,7 +111,8 @@ class Handler {
|
|||||||
await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
|
await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
|
||||||
}
|
}
|
||||||
|
|
||||||
back() {
|
back(body) {
|
||||||
|
if (body) this.response.body = body;
|
||||||
this.response.redirect = this.request.headers.referer || '/';
|
this.response.redirect = this.request.headers.referer || '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +237,9 @@ class Handler {
|
|||||||
await this.___cleanup().catch(() => { });
|
await this.___cleanup().catch(() => { });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const check = ['tid', 'rid', 'did', 'drid', 'drrid', 'psid', 'psrid'];
|
||||||
|
|
||||||
function Route(route, RouteHandler) {
|
function Route(route, RouteHandler) {
|
||||||
router.all(route, async (ctx) => {
|
router.all(route, async (ctx) => {
|
||||||
const h = new RouteHandler(ctx);
|
const h = new RouteHandler(ctx);
|
||||||
@@ -243,12 +247,16 @@ function Route(route, RouteHandler) {
|
|||||||
const method = ctx.method.toLowerCase();
|
const method = ctx.method.toLowerCase();
|
||||||
const args = { ...ctx.params, ...ctx.query, ...ctx.request.body };
|
const args = { ...ctx.params, ...ctx.query, ...ctx.request.body };
|
||||||
|
|
||||||
|
for (const l of check) {
|
||||||
|
if (args[l]) {
|
||||||
|
args[l] = new ObjectID(args[l]);
|
||||||
|
if (!args[l]) throw new ValidationError(l);
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
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);
|
||||||
if (args.rid) args.rid = new ObjectID(args.rid);
|
|
||||||
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());
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -29,9 +29,9 @@
|
|||||||
"eslint-plugin-import": "^2.20.2"
|
"eslint-plugin-import": "^2.20.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --config webpack.config.js",
|
"build": "node build/index.js",
|
||||||
"pack": "pkg .",
|
"pack": "pkg .",
|
||||||
"lint": "eslint judger --fix"
|
"lint": "eslint hydro --fix"
|
||||||
},
|
},
|
||||||
"pkg": {
|
"pkg": {
|
||||||
"scripts": [
|
"scripts": [
|
||||||
@@ -46,4 +46,4 @@
|
|||||||
"bin": {
|
"bin": {
|
||||||
"judger": "hydro/development.js"
|
"judger": "hydro/development.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="medium-9 columns">
|
<div class="medium-9 columns">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section__header">
|
<div class="section__header">
|
||||||
<h1 class="section__title">{{ _('Create Discussion in {0}').format(vnode['title']) }}</h1>
|
<h1 class="section__title">{{ _('Create Discussion in {0}').format(title) }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="section__body">
|
<div class="section__body">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
{% extends "domain_base.html" %}
|
|
||||||
{% block domain_content %}
|
|
||||||
{% if join_settings != None %}
|
|
||||||
<div class="section">
|
|
||||||
<div class="section__header">
|
|
||||||
<h1 class="section__title">{{ _('Information') }}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="section__body typo">
|
|
||||||
<p>{{ _('User can join this domain by visiting the following URL') }}:</p>
|
|
||||||
<pre><code class="language-data">{{ handler.ui_context['url_prefix'] }}{{ reverse_url('domain_join') }}</code></pre>
|
|
||||||
{% if join_settings['method'] == vj4.constant.domain.JOIN_METHOD_CODE %}
|
|
||||||
<p>{{ _('Or, with automatically filled invitation code') }}:</p>
|
|
||||||
<pre><code class="language-data">{{ handler.ui_context['url_prefix'] }}{{ reverse_url('domain_join') }}?code={{ join_settings['code']|urlencode }}</code></pre>
|
|
||||||
{% endif %}
|
|
||||||
{% if join_settings['expire'] != None %}
|
|
||||||
<br/>
|
|
||||||
<blockquote class="note">
|
|
||||||
{{ (_('The link will be expired at {0}').format(datetime_span(join_settings['expire'], relative=false)))|safe }}
|
|
||||||
</blockquote>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="section">
|
|
||||||
<div class="section__header">
|
|
||||||
<h1 class="section__title">{{ _('Settings') }}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="section__body">
|
|
||||||
<form method="POST">
|
|
||||||
{{ form.form_radio(
|
|
||||||
options=vj4.constant.domain.JOIN_METHOD_RANGE.items(),
|
|
||||||
label='Method',
|
|
||||||
name='method',
|
|
||||||
value=join_settings['method']|default(vj4.constant.domain.JOIN_METHOD_NONE),
|
|
||||||
columns=8) }}
|
|
||||||
{{ form.form_select(
|
|
||||||
options=roles_with_text,
|
|
||||||
label='Role Assignment',
|
|
||||||
name='role',
|
|
||||||
help_text='The role to assign when user joining the domain.',
|
|
||||||
value=join_settings['role']|default('')) }}
|
|
||||||
{{ form.form_select(
|
|
||||||
options=expirations.items(),
|
|
||||||
label='Expire',
|
|
||||||
name='expire',
|
|
||||||
help_text='User will no longer be allowed to join the domain after expiration.'
|
|
||||||
) }}
|
|
||||||
{{ form.form_text(
|
|
||||||
label='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.',
|
|
||||||
value=join_settings['code']|default('')) }}
|
|
||||||
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
|
|
||||||
<button class="rounded primary button" role="submit">
|
|
||||||
{{ _('Update Settings') }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
{% for role in roles %}
|
{% for role in roles %}
|
||||||
<td class="col--p">
|
<td class="col--p">
|
||||||
<label class="compact checkbox">
|
<label class="compact checkbox">
|
||||||
<input name="{{ role }}" data-checkbox-group="{{ role }}" data-checkbox-range value="{{ perm.key }}" type="checkbox"{% if bitand(handler.domain['roles'][role], perm.key) == perm.key %} checked{% endif %}>
|
<input name="{{ role }}" data-checkbox-group="{{ role }}" data-checkbox-range value="{{ perm.key }}" type="checkbox"{% if roles[role].includes(perm.key) or roles[role] == '-' %} checked{% endif %}>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -9,7 +9,12 @@
|
|||||||
<blockquote class="note">
|
<blockquote class="note">
|
||||||
<p>{{ _('Role name can only contains numbers, letters and underscores.') }}</p>
|
<p>{{ _('Role name can only contains numbers, letters and underscores.') }}</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
{{ form.form_text(columns=None, label='Name', name='role', autofocus=True) }}
|
{{ form.form_text({
|
||||||
|
columns:None,
|
||||||
|
label:'Name',
|
||||||
|
name:'role',
|
||||||
|
autofocus:true
|
||||||
|
}) }}
|
||||||
</div></div>
|
</div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,24 @@
|
|||||||
{% import "components/form.html" as form with context %}
|
{% import "components/form.html" as form with context %}
|
||||||
{% if ddoc['highlight'] or handler.hasPerm(perm.PERM_HIGHLIGHT_DISCUSSION) %}
|
{% if ddoc.highlight or handler.hasPerm(perm.PERM_HIGHLIGHT_DISCUSSION) %}
|
||||||
{{ form.form_checkbox(label='Highlight', name='highlight', value=ddoc['highlight']|default(false)) }}
|
{{ form.form_checkbox({
|
||||||
|
label:'Highlight',
|
||||||
|
name:'highlight',
|
||||||
|
value:ddoc.highlight|default(false))
|
||||||
|
}) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ form.form_text(label='Title', name='title', value=ddoc['title']|default(''), autofocus=true, required=true) }}
|
{{ form.form_text({
|
||||||
{{ form.form_textarea(columns=12, label='Content', name='content', value=ddoc['content']|default(''), hotkeys='ctrl+enter:submit', markdown=true, required=true) }}
|
label:'Title',
|
||||||
|
name:'title',
|
||||||
|
value:ddoc.title|default(''),
|
||||||
|
autofocus:true,
|
||||||
|
required:true
|
||||||
|
}) }}
|
||||||
|
{{ form.form_textarea({
|
||||||
|
columns:12,
|
||||||
|
label:'Content',
|
||||||
|
name:'content',
|
||||||
|
value:ddoc.content|default(''),
|
||||||
|
hotkeys:'ctrl+enter:submit',
|
||||||
|
markdown:true,
|
||||||
|
required:true
|
||||||
|
}) }}
|
||||||
@@ -1,46 +1,46 @@
|
|||||||
{% import "components/nothing.html" as nothing with context %}
|
{% import "components/nothing.html" as nothing with context %}
|
||||||
{% import "components/user.html" as user with context %}
|
{% import "components/user.html" as user with context %}
|
||||||
{% import "components/paginator.html" as paginator with context %}
|
{% import "components/paginator.html" as paginator with context %}
|
||||||
{% if not ddocs %}
|
{% if not ddocs.length %}
|
||||||
{{ nothing.render('no discussion yet...') }}
|
{{ nothing.render('no discussion yet...') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<ol class="section__list discussion__list">
|
<ol class="section__list discussion__list">
|
||||||
{% for ddoc in ddocs %}
|
{% for ddoc in ddocs %}
|
||||||
<li class="section__list__item discussion__item{% if ddoc['highlight'] %} highlight{% endif %}">
|
<li class="section__list__item discussion__item{% if ddoc.highlight %} highlight{% endif %}">
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media__left">
|
<div class="media__left">
|
||||||
<div class="discussion__replies numbox">
|
<div class="discussion__replies numbox">
|
||||||
<div class="numbox__num large">{{ ddoc['num_replies'] }}</div>
|
<div class="numbox__num large">{{ ddoc.nReply }}</div>
|
||||||
<div class="numbox__text">{{ _('Comments') }}</div>
|
<div class="numbox__text">{{ _('Comments') }}</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="media__body top">
|
|
||||||
<h1 class="discussion__title"><a href="{{ reverse_url('discussion_detail', did=ddoc['doc_id']) }}#{{ handler.datetime_stamp(ddoc['update_at']) if ddoc['update_at'] else '' }}" data-emoji-enabled>{{ ddoc['title'] }}</a></h1>
|
|
||||||
<ul class="supplementary list">
|
|
||||||
<li>
|
|
||||||
<a class="discussion-node-tag" href="{{ vj4.handler.discussion.node_url(handler, 'discussion_node', vj4.model.adaptor.discussion.node_id(ddoc)) }}">
|
|
||||||
{% if ddoc['parent_doc_type'] == vj4.model.document.TYPE_DISCUSSION_NODE %}
|
|
||||||
<span class="v-center icon icon-tag"></span>
|
|
||||||
{% elif ddoc['parent_doc_type'] == vj4.model.document.TYPE_CONTEST %}
|
|
||||||
<span class="v-center icon icon-award"></span>
|
|
||||||
{% else %}
|
|
||||||
<span class="v-center icon icon-flag"></span>
|
|
||||||
{% endif %}
|
|
||||||
{{ vndict[vj4.model.adaptor.discussion.node_id(ddoc)]['title']|default(_('(missing)')) }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
{{ user.render_inline(udict[ddoc['owner']], badge=false) }}
|
|
||||||
@
|
|
||||||
{{ datetime_span(ddoc['update_at'] or ddoc['_id'].generationTime) }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
{% endfor %}
|
<div class="media__body top">
|
||||||
</ol>
|
<h1 class="discussion__title"><a href="/discuss/{{ ddoc._id }}#{{ handler.datetime_stamp(ddoc.updateAt) if ddoc.update_at else '' }}" data-emoji-enabled>{{ ddoc['title'] }}</a></h1>
|
||||||
{% if page != undefined and dpcount != undefined %}
|
<ul class="supplementary list">
|
||||||
{{ paginator.render(page, dpcount) }}
|
<li>
|
||||||
{% endif %}
|
<a class="discussion-node-tag" href="/discuss/{{ ddoc.parentType }}/{{ ddoc.parentId }}">
|
||||||
{% endif %}
|
{% if ddoc.parentType == 'node' %}
|
||||||
|
<span class="v-center icon icon-tag"></span>
|
||||||
|
{% elif ddoc.parentType == 'contest' %}
|
||||||
|
<span class="v-center icon icon-award"></span>
|
||||||
|
{% else %}
|
||||||
|
<span class="v-center icon icon-flag"></span>
|
||||||
|
{% endif %}
|
||||||
|
{{ vndict[ddoc.parentId].title|default(_('(missing)')) }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{ user.render_inline(udict[ddoc.owner], badge=false) }}
|
||||||
|
@
|
||||||
|
{{ datetime_span(ddoc.updateAt)|safe }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
{% if page != undefined and dpcount != undefined %}
|
||||||
|
{{ paginator.render(page, dpcount) }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<div data-fragment-id="problem_lucky">
|
<div data-fragment-id="problem_lucky">
|
||||||
<p>
|
<p>
|
||||||
<a href="/problem/random{% if category %}?category={{ category }}{% endif %}" class="expanded button" target="_blank">
|
<a href="/problem/random{% if category %}?category={{ category }}{% endif %}" class="expanded button" target="_blank">
|
||||||
<span class="icon icon-feeling-lucky"></span> {{ _('Lucky') }}
|
<span class="icon icon-feeling-lucky"></span> {{ _('Lucky') }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<div class="typo supplementary">
|
<div class="typo supplementary">
|
||||||
<p>{{ _('Pick a problem randomly based on current filter') }}</p>
|
<p>{{ _('Pick a problem randomly based on current filter') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
<div class="section__body" data-fragment-id="problem_stat">
|
<div class="section__body" data-fragment-id="problem_stat">
|
||||||
<p>{{ _('{0} problems').format(pcount) }}</p>
|
<p>{{ _('{0} problems').format(pcount) }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
{
|
{
|
||||||
"_id": 1,
|
"_id": 1,
|
||||||
"title": "最初的最初 - A+B Problem",
|
"title": "最初的最初 - A+B Problem",
|
||||||
"require_nids": [],
|
"requireNids": [],
|
||||||
"pids": [1000, "56c52ade2fefd90539000005"]
|
"pids": [1000, "56c52ade2fefd90539000005"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_id": 2,
|
"_id": 2,
|
||||||
"title": "最初的进阶",
|
"title": "最初的进阶",
|
||||||
"require_nids": [1],
|
"requireNids": [1],
|
||||||
"pids": [1001, 1002]
|
"pids": [1001, 1002]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -8,17 +8,17 @@
|
|||||||
{{ _('no_translation_warn')|safe }}
|
{{ _('no_translation_warn')|safe }}
|
||||||
<p>每个用户可以创建自己的域。比如,老师可以为课程创建一个域,然后把题目和学生加入到此域中。再如,可以把域作为团队功能。</p>
|
<p>每个用户可以创建自己的域。比如,老师可以为课程创建一个域,然后把题目和学生加入到此域中。再如,可以把域作为团队功能。</p>
|
||||||
<p>通过合理的设置角色及其权限,可以把域设为公开或私有。</p>
|
<p>通过合理的设置角色及其权限,可以把域设为公开或私有。</p>
|
||||||
<p>默认域为Vijos,用户直接访问Vijos域名就会访问此域,点击Vijos Logo会从任意域跳转回Vijos域。</p>
|
<p>默认域为Hydro,用户直接访问Hydro域名就会访问此域,点击Hydro Logo会从任意域跳转回Hydro域。</p>
|
||||||
<p>每个域的题库、讨论、训练以及比赛是独立且自治的。</p>
|
<p>每个域的题库、讨论、训练以及比赛是独立且自治的。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
||||||
<div class="section__header">
|
<div class="section__header">
|
||||||
<h1 class="section__title" id="compiler" data-heading>{{ _('Compilers\' Version and Parameters') }}</h1>
|
<h1 class="section__title" id="compiler" data-heading>{{ _("Compilers' Version and Parameters") }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="section__body typo">
|
<div class="section__body typo">
|
||||||
{{ _('no_translation_warn')|safe }}
|
{{ _('no_translation_warn')|safe }}
|
||||||
<p>Vijos使用<a href="https://github.com/vijos/jd4">jd4</a>进行评测,以docker镜像的形式安装至评测机。所使用的编译器均为创建镜像时<a href="https://wiki.debian.org/DebianTesting">debian testing</a>分支的最新版。编译参数如下:</p>
|
<p>Hydro使用<a href="https://github.com/hydro-dev/HydroJudger">HydroJudger</a>进行评测,以docker镜像的形式安装至评测机。所使用的编译器均为创建镜像时<a href="https://wiki.debian.org/DebianTesting">debian testing</a>分支的最新版。编译参数如下:</p>
|
||||||
<blockquote><ul>
|
<blockquote><ul>
|
||||||
<li>Free Pascal <pre>fpc -O2 -o/out/foo /in/foo.pas</pre></li>
|
<li>Free Pascal <pre>fpc -O2 -o/out/foo /in/foo.pas</pre></li>
|
||||||
<li>C <pre>gcc -O2 -Wall -std=c99 -o /out/foo /in/foo.c -lm</pre></li>
|
<li>C <pre>gcc -O2 -Wall -std=c99 -o /out/foo /in/foo.c -lm</pre></li>
|
||||||
@@ -38,7 +38,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="section__body typo">
|
<div class="section__body typo">
|
||||||
{{ _('no_translation_warn')|safe }}
|
{{ _('no_translation_warn')|safe }}
|
||||||
<p>Vijos 评测机使用进程的CPU时间计算时间消耗,时间的限定为题目中评测点所指定的时间。</p><p>Vijos 评测机使用进程虚拟内存与物理内存总和计算内存空间消耗。内存空间默认限定为256MiB,题目中特别指明的,限定为题目中评测点所指定的内存空间。</p>
|
<p>Hydro 评测机使用进程的CPU时间计算时间消耗,时间的限定为题目中评测点所指定的时间。</p>
|
||||||
|
<p>Hydro 评测机使用进程虚拟内存与物理内存总和计算内存空间消耗。内存空间默认限定为256MiB,题目中特别指明的,限定为题目中评测点所指定的内存空间。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
||||||
@@ -47,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="section__body typo">
|
<div class="section__body typo">
|
||||||
{{ _('no_translation_warn')|safe }}
|
{{ _('no_translation_warn')|safe }}
|
||||||
<p>Vijos 支持且仅支持标准输入输出(控制台输入输出,屏幕输入输出,STD I/O),不支持文件输入输出。</p>
|
<p>若无特殊说明,Hydro 使用标准输入输出(控制台输入输出,屏幕输入输出,STD I/O)。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
||||||
@@ -130,7 +131,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="section__body typo">
|
<div class="section__body typo">
|
||||||
{{ _('no_translation_warn')|safe }}
|
{{ _('no_translation_warn')|safe }}
|
||||||
<p>通过率的影响极其恶劣,现在 Vijos 不提供也不承认通过率。</p>
|
<p>通过率的影响极其恶劣,现在 Hydro 不提供也不承认通过率。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
||||||
@@ -139,7 +140,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="section__body typo">
|
<div class="section__body typo">
|
||||||
{{ _('no_translation_warn')|safe }}
|
{{ _('no_translation_warn')|safe }}
|
||||||
<p>Vijos RP 分为固定 RP 与浮动 RP ,固定 RP 可由活动、比赛等的奖励获得,浮动 RP 由一般性通过题目获得。</p>
|
<p>Hydro RP 分为固定 RP 与浮动 RP ,固定 RP 可由活动、比赛等的奖励获得,浮动 RP 由一般性通过题目获得。</p>
|
||||||
<p>浮动 RP 是一个动态的数值,每个用户在每道题上获得的 RP 都不一样,并且未来也不固定(即浮动之意)。</p>
|
<p>浮动 RP 是一个动态的数值,每个用户在每道题上获得的 RP 都不一样,并且未来也不固定(即浮动之意)。</p>
|
||||||
<blockquote class="note">每道题的浮动 RP 由该题通过总人数与该用户通过此题的名次计算得来。</blockquote>
|
<blockquote class="note">每道题的浮动 RP 由该题通过总人数与该用户通过此题的名次计算得来。</blockquote>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,7 +151,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="section__body typo">
|
<div class="section__body typo">
|
||||||
{{ _('no_translation_warn')|safe }}
|
{{ _('no_translation_warn')|safe }}
|
||||||
<p>Vijos 中题目的难度,根据递交数、通过率以及每个递交的递交时间和评测结果,通过算法计算得出。</p>
|
<p>Hydro 中题目的难度,根据递交数、通过率以及每个递交的递交时间和评测结果,通过算法计算得出。</p>
|
||||||
<p>因此,请注意以下几点:</p>
|
<p>因此,请注意以下几点:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>0. 一般地,难度的数值越大,该题目越难。</li>
|
<li>0. 一般地,难度的数值越大,该题目越难。</li>
|
||||||
|
|||||||
@@ -17,22 +17,6 @@ const mapStateToProps = (state) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
handleClickAdd() {
|
|
||||||
dispatch({
|
|
||||||
type: 'SCRATCHPAD_PRETEST_ADD_DATA',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleClickRemove() {
|
|
||||||
dispatch({
|
|
||||||
type: 'SCRATCHPAD_PRETEST_REMOVE_DATA',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleSwitchData(id) {
|
|
||||||
dispatch({
|
|
||||||
type: 'SCRATCHPAD_PRETEST_SWITCH_TO_DATA',
|
|
||||||
payload: id,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleClickClose() {
|
handleClickClose() {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SCRATCHPAD_UI_SET_VISIBILITY',
|
type: 'SCRATCHPAD_UI_SET_VISIBILITY',
|
||||||
@@ -66,18 +50,6 @@ export default class ScratchpadPretestContainer extends React.PureComponent {
|
|||||||
<ScrollableInkTabBar
|
<ScrollableInkTabBar
|
||||||
extraContent={(
|
extraContent={(
|
||||||
<span>
|
<span>
|
||||||
<PanelButton
|
|
||||||
data-tooltip={i18n('Add new data')}
|
|
||||||
onClick={() => this.props.handleClickAdd()}
|
|
||||||
>
|
|
||||||
{i18n('Add')}
|
|
||||||
</PanelButton>
|
|
||||||
<PanelButton
|
|
||||||
data-tooltip={i18n('Remove this data')}
|
|
||||||
onClick={() => this.props.handleClickRemove()}
|
|
||||||
>
|
|
||||||
{i18n('Remove')}
|
|
||||||
</PanelButton>
|
|
||||||
<PanelButton
|
<PanelButton
|
||||||
onClick={() => this.props.handleClickClose()}
|
onClick={() => this.props.handleClickClose()}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import * as recordEnum from 'vj/constant/record';
|
|||||||
|
|
||||||
const shouldShowDetail = (data) => recordEnum.STATUS_SCRATCHPAD_SHOW_DETAIL_FLAGS[data.status];
|
const shouldShowDetail = (data) => recordEnum.STATUS_SCRATCHPAD_SHOW_DETAIL_FLAGS[data.status];
|
||||||
|
|
||||||
const isPretest = (data) => data.type === recordEnum.TYPE_PRETEST;
|
const isPretest = (data) => data.pretest === true;
|
||||||
|
|
||||||
const getRecordDetail = (data) => {
|
const getRecordDetail = (data) => {
|
||||||
if (!shouldShowDetail(data)) {
|
if (!shouldShowDetail(data)) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
const initialId = uuid();
|
const initialId = uuid();
|
||||||
|
|
||||||
@@ -23,67 +22,6 @@ export default function reducer(state = {
|
|||||||
},
|
},
|
||||||
}, action) {
|
}, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SCRATCHPAD_PRETEST_SWITCH_TO_DATA': {
|
|
||||||
const currentId = action.payload;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
current: currentId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'SCRATCHPAD_PRETEST_ADD_DATA': {
|
|
||||||
const newCounter = state.counter + 1;
|
|
||||||
const newId = uuid();
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
counter: newCounter,
|
|
||||||
current: newId,
|
|
||||||
tabs: [...state.tabs, newId],
|
|
||||||
meta: {
|
|
||||||
...state.meta,
|
|
||||||
[newId]: {
|
|
||||||
id: newId,
|
|
||||||
title: `#${newCounter}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
...state.data,
|
|
||||||
[newId]: {
|
|
||||||
id: newId,
|
|
||||||
input: '',
|
|
||||||
output: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'SCRATCHPAD_PRETEST_REMOVE_DATA': {
|
|
||||||
const orgIdx = state.tabs.indexOf(state.current);
|
|
||||||
let newCounter = state.counter;
|
|
||||||
const newTabs = _.without(state.tabs, state.current);
|
|
||||||
const newMeta = _.omit(state.meta, state.current);
|
|
||||||
const newData = _.omit(state.data, state.current);
|
|
||||||
if (newTabs.length === 0) {
|
|
||||||
// keep at least one data
|
|
||||||
const id = uuid();
|
|
||||||
newTabs.push(id);
|
|
||||||
newMeta[id] = {
|
|
||||||
id,
|
|
||||||
title: `#${++newCounter}`,
|
|
||||||
};
|
|
||||||
newData[id] = {
|
|
||||||
input: '',
|
|
||||||
output: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const newIdx = (orgIdx < newTabs.length) ? orgIdx : orgIdx - 1;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
counter: newCounter,
|
|
||||||
current: newTabs[newIdx],
|
|
||||||
tabs: newTabs,
|
|
||||||
meta: newMeta,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'SCRATCHPAD_PRETEST_DATA_CHANGE': {
|
case 'SCRATCHPAD_PRETEST_DATA_CHANGE': {
|
||||||
const { id, type, value } = action.payload;
|
const { id, type, value } = action.payload;
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
const config = {
|
|
||||||
mode: 'production',
|
|
||||||
entry: {
|
|
||||||
development: './hydro/development.js',
|
|
||||||
install: './hydro/install.js',
|
|
||||||
uninstall: './hydro/uninstall.js'
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
filename: '[name].js',
|
|
||||||
path: __dirname + '/dist'
|
|
||||||
},
|
|
||||||
target: 'node',
|
|
||||||
module: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = config;
|
|
||||||
Reference in new issue
Block a user