mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
添加功能:pretest discuss user dashboard statistics
This commit is contained in:
1 parent
61cba6f180
commit
15d43636c0
39 files changed
+862
-465
No files matched your search
+2
-1
@@ -24,6 +24,7 @@ module.exports = {
|
||||
"radix": "off",
|
||||
"guard-for-in": "off",
|
||||
"no-param-reassign": "off",
|
||||
"global-require": "off"
|
||||
"global-require": "off",
|
||||
"no-nested-ternary": "off",
|
||||
},
|
||||
};
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
node_modules/
|
||||
config.yaml
|
||||
.build/
|
||||
.uibuild/
|
||||
__*
|
||||
ui/misc/.iconfont
|
||||
ui/static/locale
|
||||
.webpackStats.json
|
||||
.cache
|
||||
dist/
|
||||
.webpackStats.json
|
||||
Vendored
+2
-1
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"files.autoGuessEncoding": false,
|
||||
"files.autoSave": "onFocusChange",
|
||||
"files.encoding": "utf8",
|
||||
"files.autoGuessEncoding": false,
|
||||
"files.eol": "\n",
|
||||
"editor.detectIndentation": 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;
|
||||
+11
-20
@@ -8,25 +8,24 @@ process.stdin.on('data', async (input) => {
|
||||
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');
|
||||
i18n(path.resolve(__dirname, '..', 'locales', 'zh_TW.yaml'), 'zh_TW');
|
||||
i18n(path.resolve(__dirname, '..', 'locales', 'en.yaml'), 'en');
|
||||
|
||||
global.bus = new EventEmitter();
|
||||
async function run() {
|
||||
require('./lib/i18n');
|
||||
require('./utils');
|
||||
require('./service/db');
|
||||
|
||||
const bus = require('./service/bus');
|
||||
|
||||
async function run() {
|
||||
await new Promise((resolve) => {
|
||||
global.bus.once('connected', () => {
|
||||
const h = () => {
|
||||
console.log('Database connected');
|
||||
bus.unsubscribe(['system_database_connected'], h);
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
bus.subscribe(['system_database_connected'], h);
|
||||
require('./service/db');
|
||||
});
|
||||
require('./service/gridfs');
|
||||
require('./service/queue');
|
||||
const server = require('./service/server');
|
||||
require('./handler/home');
|
||||
require('./handler/problem');
|
||||
@@ -38,14 +37,6 @@ async function run() {
|
||||
require('./handler/import');
|
||||
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) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
|
||||
+65
-94
@@ -6,6 +6,7 @@ class UserFacingError extends Error {
|
||||
this.params = [];
|
||||
}
|
||||
}
|
||||
|
||||
class SystemError extends Error {
|
||||
constructor(type) {
|
||||
super(type);
|
||||
@@ -14,70 +15,89 @@ class SystemError extends Error {
|
||||
this.params = [];
|
||||
}
|
||||
}
|
||||
|
||||
class BadRequestError extends UserFacingError {
|
||||
constructor(type) {
|
||||
super(type);
|
||||
this.code = 400;
|
||||
}
|
||||
}
|
||||
|
||||
class ForbiddenError extends UserFacingError {
|
||||
constructor(type) {
|
||||
super(type);
|
||||
this.code = 403;
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends UserFacingError {
|
||||
constructor(type) {
|
||||
super(type || 'NotFoundError');
|
||||
this.code = 404;
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteOnlineJudgeError extends UserFacingError {
|
||||
constructor(message) {
|
||||
super('RemoteOnlineJudgeError');
|
||||
this.params = [message];
|
||||
}
|
||||
}
|
||||
|
||||
class AlreadyVotedError extends BadRequestError {
|
||||
constructor(psid, uid) {
|
||||
super('You\'ve already voted.');
|
||||
this.params = [psid, uid];
|
||||
}
|
||||
}
|
||||
|
||||
class LoginError extends ForbiddenError {
|
||||
constructor(uname) {
|
||||
super('LoginError');
|
||||
this.params = [uname];
|
||||
}
|
||||
}
|
||||
|
||||
class UserAlreadyExistError extends ForbiddenError {
|
||||
constructor(uname) {
|
||||
super('UserAlreadyExistError');
|
||||
this.params = [uname];
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidTokenError extends ForbiddenError {
|
||||
constructor(token) {
|
||||
super('InvalidTokenError');
|
||||
this.params = [token];
|
||||
}
|
||||
}
|
||||
|
||||
class BlacklistedError extends ForbiddenError {
|
||||
constructor(ip) {
|
||||
super('Address {0} is blacklisted.');
|
||||
this.params = [ip];
|
||||
}
|
||||
}
|
||||
|
||||
class UserNotFoundError extends NotFoundError {
|
||||
constructor(user) {
|
||||
super('UserNotFoundError');
|
||||
this.params = [user];
|
||||
}
|
||||
}
|
||||
|
||||
class NoProblemError extends NotFoundError {
|
||||
constructor() {
|
||||
super('NoProblemError');
|
||||
}
|
||||
}
|
||||
|
||||
class VerifyPasswordError extends ForbiddenError {
|
||||
constructor() {
|
||||
super('VerifyPasswordError');
|
||||
}
|
||||
}
|
||||
|
||||
class OpcountExceededError extends ForbiddenError {
|
||||
constructor(op, periodSecs, maxOperations) {
|
||||
super('OpcountExceededError');
|
||||
@@ -91,6 +111,7 @@ class PermissionError extends ForbiddenError {
|
||||
this.params = [perm];
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError extends ForbiddenError {
|
||||
constructor(field0, field1) {
|
||||
super('ValidationError');
|
||||
@@ -98,66 +119,84 @@ class ValidationError extends ForbiddenError {
|
||||
else this.params = [field0, field1];
|
||||
}
|
||||
}
|
||||
|
||||
class ContestNotAttendedError extends ForbiddenError {
|
||||
constructor(tid) {
|
||||
super('You haven\'t attended this contest yet.');
|
||||
this.params = [tid];
|
||||
}
|
||||
}
|
||||
|
||||
class ContestAlreadyAttendedError extends ForbiddenError {
|
||||
constructor(tid, uid) {
|
||||
super('You\'ve already attended this contest.');
|
||||
this.params = [tid, uid];
|
||||
}
|
||||
}
|
||||
|
||||
class ContestNotLiveError extends ForbiddenError {
|
||||
constructor(tid) {
|
||||
super('This contest is not live.');
|
||||
this.params = [tid];
|
||||
}
|
||||
}
|
||||
|
||||
class ContestScoreboardHiddenError extends ForbiddenError {
|
||||
constructor(tid) {
|
||||
super('Contest scoreboard is not visible.');
|
||||
this.params = [tid];
|
||||
}
|
||||
}
|
||||
|
||||
class TrainingAlreadyEnrollError extends ForbiddenError {
|
||||
constructor(tid, uid) {
|
||||
super("You've already enrolled this training.");
|
||||
this.params = [tid, uid];
|
||||
}
|
||||
}
|
||||
|
||||
class RoleAlreadyExistError extends ForbiddenError {
|
||||
constructor(role) {
|
||||
super('This role already exists.');
|
||||
this.params = [role];
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemNotFoundError extends NotFoundError {
|
||||
constructor(pid) {
|
||||
super('ProblemNotFoundError');
|
||||
this.params = [pid];
|
||||
}
|
||||
}
|
||||
|
||||
class RecordNotFoundError extends NotFoundError {
|
||||
constructor(rid) {
|
||||
super('RecordNotFoundError');
|
||||
this.params = [rid];
|
||||
}
|
||||
}
|
||||
|
||||
class SolutionNotFoundError extends NotFoundError {
|
||||
constructor(psid) {
|
||||
super('SolutionNotFoundError');
|
||||
this.params = [psid];
|
||||
}
|
||||
}
|
||||
|
||||
class TrainingNotFoundError extends NotFoundError {
|
||||
constructor(tid) {
|
||||
super('TrainingNotFoundError');
|
||||
this.params = [tid];
|
||||
}
|
||||
}
|
||||
|
||||
class ContestNotFoundError extends NotFoundError {
|
||||
constructor(cid) {
|
||||
super('ContestNotFoundError');
|
||||
this.params = [cid];
|
||||
}
|
||||
}
|
||||
|
||||
class ProblemDataNotFoundError extends NotFoundError {
|
||||
constructor(pid) {
|
||||
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 = {
|
||||
BadRequestError,
|
||||
BlacklistedError,
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
LoginError,
|
||||
@@ -193,49 +254,13 @@ module.exports = {
|
||||
SystemError,
|
||||
TrainingAlreadyEnrollError,
|
||||
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):
|
||||
@property
|
||||
def message(self):
|
||||
@@ -253,13 +278,6 @@ class UnknownFieldError(ForbiddenError):
|
||||
def message(self):
|
||||
return 'Unknown field {0}.'
|
||||
|
||||
|
||||
class DocumentNotFoundError(NotFoundError):
|
||||
@property
|
||||
def message(self):
|
||||
return 'Document {2} not found.'
|
||||
|
||||
|
||||
class CsrfTokenError(ForbiddenError):
|
||||
pass
|
||||
|
||||
@@ -296,54 +314,12 @@ class DiscussionNodeAlreadyExistError(ForbiddenError):
|
||||
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):
|
||||
@property
|
||||
def message(self):
|
||||
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):
|
||||
@property
|
||||
def message(self):
|
||||
@@ -355,11 +331,6 @@ class UsageExceededError(ForbiddenError):
|
||||
def message(self):
|
||||
return 'Usage exceeded.'
|
||||
|
||||
class ModifyBuiltinRoleError(ForbiddenError):
|
||||
@property
|
||||
def message(self):
|
||||
return 'Built-in roles cannot be modified.'
|
||||
|
||||
|
||||
class InvalidArgumentError(BadRequestError):
|
||||
@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 {
|
||||
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'));
|
||||
if (!url.endsWith('/')) url += '/';
|
||||
const [, host, pid] = RE_SYZOJ.exec(url);
|
||||
|
||||
+29
-3
@@ -96,8 +96,11 @@ class JudgeHandler extends Handler {
|
||||
let rid = await queue.get('judge', false);
|
||||
while (rid) {
|
||||
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
|
||||
const task = {
|
||||
let task;
|
||||
if (!rdoc.input) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const pdoc = await problem.getById(rdoc.pid);
|
||||
task = {
|
||||
event: 'judge',
|
||||
rid,
|
||||
type: 0,
|
||||
@@ -106,6 +109,16 @@ class JudgeHandler extends Handler {
|
||||
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);
|
||||
rid = await queue.get('judge', false); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
@@ -134,8 +147,11 @@ class JudgeConnectionHandler extends ConnectionHandler {
|
||||
this.processing = null;
|
||||
const rid = await queue.get('judge');
|
||||
const rdoc = await record.get(rid);
|
||||
let task;
|
||||
if (!rdoc.input) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const pdoc = await problem.getById(rdoc.pid);
|
||||
const task = {
|
||||
task = {
|
||||
event: 'judge',
|
||||
rid,
|
||||
type: 0,
|
||||
@@ -144,6 +160,16 @@ class JudgeConnectionHandler extends ConnectionHandler {
|
||||
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.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 solution = require('../model/solution');
|
||||
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 queue = require('../service/queue');
|
||||
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 {
|
||||
async prepare() {
|
||||
if (this.pdoc.owner !== this.uid) this.checkPerm(PERM_EDIT_PROBLEM);
|
||||
@@ -322,7 +367,7 @@ class ProblemCreateHandler extends Handler {
|
||||
title, pid, content, hidden,
|
||||
}) {
|
||||
validator.checkPid(pid);
|
||||
pid = pid || await system.incPidCounter();
|
||||
pid = pid || await system.inc('pid');
|
||||
await problem.add({
|
||||
title, content, owner: this.user._id, pid, hidden,
|
||||
});
|
||||
@@ -335,7 +380,9 @@ Route('/p', ProblemHandler);
|
||||
Route('/problem/random', ProblemRandomHandler);
|
||||
Route('/p/:pid', ProblemDetailHandler);
|
||||
Route('/p/:pid/submit', ProblemSubmitHandler);
|
||||
Route('/p/:pid/pretest', ProblemPretestHandler);
|
||||
Route('/p/:pid/settings', ProblemSettingsHandler);
|
||||
Route('/p/:pid/statistics', ProblemStatisticsHandler);
|
||||
Route('/p/:pid/edit', ProblemEditHandler);
|
||||
Route('/p/:pid/upload', ProblemDataUploadHandler);
|
||||
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/:psrid/raw', ProblemSolutionReplyRawHandler);
|
||||
Route('/problem/create', ProblemCreateHandler);
|
||||
|
||||
|
||||
/*
|
||||
|
||||
@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)
|
||||
|
||||
*/
|
||||
Connection('/p/:pid/pretest-conn', ProblemPretestConnectionHandler);
|
||||
@@ -96,6 +96,7 @@ class RecordConnectionHandler extends ConnectionHandler {
|
||||
async onRecordChange(data) {
|
||||
const rdoc = data.value;
|
||||
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)]);
|
||||
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 }) });
|
||||
|
||||
@@ -91,7 +91,7 @@ class UserRegisterWithCodeHandler extends Handler {
|
||||
const { mail } = await token.get(code, token.TYPE_REGISTRATION);
|
||||
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
|
||||
if (password !== verifyPassword) throw new VerifyPasswordError();
|
||||
const uid = await system.incUserCounter();
|
||||
const uid = await system.inc('user');
|
||||
await user.create({
|
||||
uid, uname, password, mail, regip: this.request.ip,
|
||||
});
|
||||
|
||||
+2
-9
@@ -1,7 +1,4 @@
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const locales = {};
|
||||
const locales = require('../../.build/locales.json');
|
||||
|
||||
String.prototype.format = function formatStr(...args) {
|
||||
let result = this;
|
||||
@@ -33,8 +30,4 @@ String.prototype.translate = function translate(language = 'zh_CN') {
|
||||
return this;
|
||||
};
|
||||
|
||||
module.exports = function load(file, language) {
|
||||
if (!locales[language]) locales[language] = {};
|
||||
const content = fs.readFileSync(file).toString();
|
||||
Object.assign(locales[language], yaml.safeLoad(content));
|
||||
};
|
||||
module.exports = locales;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
/* eslint-disable no-bitwise */ /* eslint-disable max-len */
|
||||
function safeAdd(x, y) {
|
||||
const lsw = (x & 0xffff) + (y & 0xffff);
|
||||
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;
|
||||
}
|
||||
|
||||
async function get(psid) {
|
||||
psid = new ObjectID(psid);
|
||||
const psdoc = await coll.findOne({ _id: psid });
|
||||
if (!psdoc) throw new SolutionNotFoundError();
|
||||
return psdoc;
|
||||
}
|
||||
|
||||
function getMany(query, sort, page, limit) {
|
||||
return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit)
|
||||
.toArray();
|
||||
}
|
||||
|
||||
async function edit(_id, content) {
|
||||
validator.checkContent(content);
|
||||
await coll.findOneAndUpdate({ _id }, { $set: { content } });
|
||||
@@ -36,15 +39,19 @@ async function edit(_id, content) {
|
||||
if (!psdoc) throw new SolutionNotFoundError(_id);
|
||||
return psdoc;
|
||||
}
|
||||
|
||||
function del(psid) {
|
||||
return coll.deleteOne({ _id: psid });
|
||||
}
|
||||
|
||||
function count(query) {
|
||||
return coll.find(query).count();
|
||||
}
|
||||
|
||||
function getMulti(pid) {
|
||||
return coll.find({ pid }).sort({ vote: -1 });
|
||||
}
|
||||
|
||||
function reply(psid, owner, content) {
|
||||
validator.checkContent(content);
|
||||
return coll.findOneAndUpdate(
|
||||
@@ -52,18 +59,20 @@ function reply(psid, owner, content) {
|
||||
{ $push: { reply: { content, owner, _id: new ObjectID() } } },
|
||||
);
|
||||
}
|
||||
|
||||
async function getReply(psid, psrid) {
|
||||
const psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } });
|
||||
if (!psdoc) return [null, null];
|
||||
for (const psrdoc of psdoc) if (psrdoc._id === psrid) return [psdoc, psrdoc];
|
||||
return [psdoc, null];
|
||||
}
|
||||
|
||||
async function editReply(psid, psrid, content) {
|
||||
validator.checkContent(content);
|
||||
psid = new ObjectID(psid);
|
||||
psrid = new ObjectID(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) {
|
||||
if (reply[i]._id === psrid) {
|
||||
reply[i].content = content;
|
||||
@@ -72,9 +81,11 @@ async function editReply(psid, psrid, content) {
|
||||
}
|
||||
return await coll.findOneAndUpdate({ _id: psdoc._id }, { $set: { reply } });
|
||||
}
|
||||
|
||||
function delReply(psid, psrid) {
|
||||
return coll.findOneAndUpdate({ _id: psid }, { $pull: { reply: { _id: psrid } } });
|
||||
}
|
||||
|
||||
async function vote(psid, uid, value) {
|
||||
let pssdoc = await collStatus.findOne({ 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 });
|
||||
return [psdoc, pssdoc];
|
||||
}
|
||||
|
||||
async function getListStatus(list, uid) {
|
||||
const result = {};
|
||||
const res = await collStatus.find({ uid, psid: { $in: list } }).toArray();
|
||||
for (const i of res) result[i.psid] = i;
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
count,
|
||||
add,
|
||||
|
||||
+9
-30
@@ -11,41 +11,20 @@ async function update(_id, operation, config) {
|
||||
await coll.findOneAndUpdate({ _id }, operation, config);
|
||||
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.
|
||||
*/
|
||||
function incUserCounter() {
|
||||
return update('userCounter', { $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;
|
||||
function inc(field) {
|
||||
return update(field, { $inc: { value: 1 } }, { upsert: true });
|
||||
}
|
||||
module.exports = {
|
||||
get,
|
||||
update,
|
||||
incPidCounter,
|
||||
incUserCounter,
|
||||
acquireLock,
|
||||
releaseLock,
|
||||
releaseLockAnyway,
|
||||
inc,
|
||||
set,
|
||||
};
|
||||
@@ -9,15 +9,18 @@ const collStatus = db.collection('training.status');
|
||||
function getStatus(tid, uid) {
|
||||
return collStatus.findOne({ tid, uid });
|
||||
}
|
||||
|
||||
function getMultiStatus(query) {
|
||||
return collStatus.find(query);
|
||||
}
|
||||
|
||||
async function getListStatus(uid, tids) {
|
||||
const tsdocs = await getMultiStatus({ uid, tid: { $in: Array.from(new Set(tids)) } }).toArray();
|
||||
const r = {};
|
||||
for (const tsdoc of tsdocs) r[tsdoc.pid] = tsdoc;
|
||||
return r;
|
||||
}
|
||||
|
||||
async function enroll(tid, uid) {
|
||||
try {
|
||||
await collStatus.insertOne({ tid, uid, enroll: 1 });
|
||||
@@ -26,6 +29,7 @@ async function enroll(tid, uid) {
|
||||
}
|
||||
await coll.findOneAndUpdate({ _id: tid }, { $inc: { enroll: 1 } });
|
||||
}
|
||||
|
||||
async function setStatus(tid, uid, $set) {
|
||||
await collStatus.findOneAndUpdate({ tid, uid }, { $set });
|
||||
return await collStatus.findOne({ tid, uid });
|
||||
|
||||
+7
-2
@@ -83,7 +83,7 @@ async function changePassword(uid, currentPassword, newPassword) {
|
||||
const udoc = await getById(uid);
|
||||
udoc.checkPassword(currentPassword);
|
||||
const salt = pwhash.salt();
|
||||
return await coll.findOneAndUpdate({
|
||||
return await coll.findOneAndUpdate({ // eslint-disable-line no-return-await
|
||||
_id: udoc._id,
|
||||
}, {
|
||||
$set: { salt, hash: pwhash.hash(newPassword, salt) },
|
||||
@@ -101,7 +101,7 @@ async function create({
|
||||
validator.checkPassword(password);
|
||||
validator.checkEmail(mail);
|
||||
const salt = pwhash.salt();
|
||||
if (!uid) uid = system.incUserCounter();
|
||||
if (!uid) uid = system.inc('user');
|
||||
try {
|
||||
await coll.insertOne({
|
||||
_id: uid,
|
||||
@@ -127,6 +127,10 @@ function getMany(params) {
|
||||
return coll.find(params);
|
||||
}
|
||||
|
||||
function getRoles() {
|
||||
return collRole.find().toArray();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
changePassword,
|
||||
create,
|
||||
@@ -139,4 +143,5 @@ module.exports = {
|
||||
setEmail,
|
||||
setPassword,
|
||||
getList,
|
||||
getRoles,
|
||||
};
|
||||
@@ -58,4 +58,5 @@ try {
|
||||
} catch (e) {
|
||||
console.error('Cannot load config');
|
||||
}
|
||||
|
||||
module.exports = options;
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
const Mongo = require('mongodb');
|
||||
const bus = require('../service/bus');
|
||||
const options = require('../options');
|
||||
|
||||
let mongourl = 'mongodb://';
|
||||
@@ -12,7 +13,7 @@ Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology:
|
||||
.then((Database) => {
|
||||
db = Database.db(options.db.name);
|
||||
db.Db = Database;
|
||||
global.bus.emit('connected');
|
||||
bus.publish('system_database_connected', null);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
|
||||
+12
-4
@@ -17,7 +17,7 @@ const token = require('../model/token');
|
||||
const opcount = require('../model/opcount');
|
||||
const {
|
||||
UserNotFoundError, BlacklistedError, PermissionError,
|
||||
NotFoundError, UserFacingError,
|
||||
NotFoundError, UserFacingError, ValidationError,
|
||||
} = require('../error');
|
||||
|
||||
const app = new Koa();
|
||||
@@ -111,7 +111,8 @@ class Handler {
|
||||
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 || '/';
|
||||
}
|
||||
|
||||
@@ -236,6 +237,9 @@ class Handler {
|
||||
await this.___cleanup().catch(() => { });
|
||||
}
|
||||
}
|
||||
|
||||
const check = ['tid', 'rid', 'did', 'drid', 'drrid', 'psid', 'psrid'];
|
||||
|
||||
function Route(route, RouteHandler) {
|
||||
router.all(route, async (ctx) => {
|
||||
const h = new RouteHandler(ctx);
|
||||
@@ -243,12 +247,16 @@ function Route(route, RouteHandler) {
|
||||
const method = ctx.method.toLowerCase();
|
||||
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.title) validator.checkContent(args.title);
|
||||
if (args.uid) args.uid = parseInt(validator.checkUid(args.uid));
|
||||
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.pids) args.pids = args.pids.split(',').map((i) => i.trim());
|
||||
|
||||
|
||||
+2
-2
@@ -29,9 +29,9 @@
|
||||
"eslint-plugin-import": "^2.20.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"build": "node build/index.js",
|
||||
"pack": "pkg .",
|
||||
"lint": "eslint judger --fix"
|
||||
"lint": "eslint hydro --fix"
|
||||
},
|
||||
"pkg": {
|
||||
"scripts": [
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="medium-9 columns">
|
||||
<div class="section">
|
||||
<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 class="section__body">
|
||||
<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 %}
|
||||
<td class="col--p">
|
||||
<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>
|
||||
</td>
|
||||
{% endfor %}
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
<blockquote class="note">
|
||||
<p>{{ _('Role name can only contains numbers, letters and underscores.') }}</p>
|
||||
</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>
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
{% import "components/form.html" as form with context %}
|
||||
{% if ddoc['highlight'] or handler.hasPerm(perm.PERM_HIGHLIGHT_DISCUSSION) %}
|
||||
{{ form.form_checkbox(label='Highlight', name='highlight', value=ddoc['highlight']|default(false)) }}
|
||||
{% if ddoc.highlight or handler.hasPerm(perm.PERM_HIGHLIGHT_DISCUSSION) %}
|
||||
{{ form.form_checkbox({
|
||||
label:'Highlight',
|
||||
name:'highlight',
|
||||
value:ddoc.highlight|default(false))
|
||||
}) }}
|
||||
{% endif %}
|
||||
{{ form.form_text(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) }}
|
||||
{{ form.form_text({
|
||||
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,38 +1,38 @@
|
||||
{% import "components/nothing.html" as nothing with context %}
|
||||
{% import "components/user.html" as user with context %}
|
||||
{% import "components/paginator.html" as paginator with context %}
|
||||
{% if not ddocs %}
|
||||
{% if not ddocs.length %}
|
||||
{{ nothing.render('no discussion yet...') }}
|
||||
{% else %}
|
||||
<ol class="section__list discussion__list">
|
||||
{% 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__left">
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
<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 %}
|
||||
<a class="discussion-node-tag" href="/discuss/{{ ddoc.parentType }}/{{ ddoc.parentId }}">
|
||||
{% if ddoc.parentType == 'node' %}
|
||||
<span class="v-center icon icon-tag"></span>
|
||||
{% elif ddoc['parent_doc_type'] == vj4.model.document.TYPE_CONTEST %}
|
||||
{% elif ddoc.parentType == '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)')) }}
|
||||
{{ vndict[ddoc.parentId].title|default(_('(missing)')) }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{ user.render_inline(udict[ddoc['owner']], badge=false) }}
|
||||
{{ user.render_inline(udict[ddoc.owner], badge=false) }}
|
||||
@
|
||||
{{ datetime_span(ddoc['update_at'] or ddoc['_id'].generationTime) }}
|
||||
{{ datetime_span(ddoc.updateAt)|safe }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
{
|
||||
"_id": 1,
|
||||
"title": "最初的最初 - A+B Problem",
|
||||
"require_nids": [],
|
||||
"requireNids": [],
|
||||
"pids": [1000, "56c52ade2fefd90539000005"]
|
||||
},
|
||||
{
|
||||
"_id": 2,
|
||||
"title": "最初的进阶",
|
||||
"require_nids": [1],
|
||||
"requireNids": [1],
|
||||
"pids": [1001, 1002]
|
||||
}
|
||||
]
|
||||
@@ -8,17 +8,17 @@
|
||||
{{ _('no_translation_warn')|safe }}
|
||||
<p>每个用户可以创建自己的域。比如,老师可以为课程创建一个域,然后把题目和学生加入到此域中。再如,可以把域作为团队功能。</p>
|
||||
<p>通过合理的设置角色及其权限,可以把域设为公开或私有。</p>
|
||||
<p>默认域为Vijos,用户直接访问Vijos域名就会访问此域,点击Vijos Logo会从任意域跳转回Vijos域。</p>
|
||||
<p>默认域为Hydro,用户直接访问Hydro域名就会访问此域,点击Hydro Logo会从任意域跳转回Hydro域。</p>
|
||||
<p>每个域的题库、讨论、训练以及比赛是独立且自治的。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
||||
<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 class="section__body typo">
|
||||
{{ _('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>
|
||||
<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>
|
||||
@@ -38,7 +38,8 @@
|
||||
</div>
|
||||
<div class="section__body typo">
|
||||
{{ _('no_translation_warn')|safe }}
|
||||
<p>Vijos 评测机使用进程的CPU时间计算时间消耗,时间的限定为题目中评测点所指定的时间。</p><p>Vijos 评测机使用进程虚拟内存与物理内存总和计算内存空间消耗。内存空间默认限定为256MiB,题目中特别指明的,限定为题目中评测点所指定的内存空间。</p>
|
||||
<p>Hydro 评测机使用进程的CPU时间计算时间消耗,时间的限定为题目中评测点所指定的时间。</p>
|
||||
<p>Hydro 评测机使用进程虚拟内存与物理内存总和计算内存空间消耗。内存空间默认限定为256MiB,题目中特别指明的,限定为题目中评测点所指定的内存空间。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
||||
@@ -47,7 +48,7 @@
|
||||
</div>
|
||||
<div class="section__body typo">
|
||||
{{ _('no_translation_warn')|safe }}
|
||||
<p>Vijos 支持且仅支持标准输入输出(控制台输入输出,屏幕输入输出,STD I/O),不支持文件输入输出。</p>
|
||||
<p>若无特殊说明,Hydro 使用标准输入输出(控制台输入输出,屏幕输入输出,STD I/O)。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
||||
@@ -130,7 +131,7 @@
|
||||
</div>
|
||||
<div class="section__body typo">
|
||||
{{ _('no_translation_warn')|safe }}
|
||||
<p>通过率的影响极其恶劣,现在 Vijos 不提供也不承认通过率。</p>
|
||||
<p>通过率的影响极其恶劣,现在 Hydro 不提供也不承认通过率。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
||||
@@ -139,7 +140,7 @@
|
||||
</div>
|
||||
<div class="section__body typo">
|
||||
{{ _('no_translation_warn')|safe }}
|
||||
<p>Vijos RP 分为固定 RP 与浮动 RP ,固定 RP 可由活动、比赛等的奖励获得,浮动 RP 由一般性通过题目获得。</p>
|
||||
<p>Hydro RP 分为固定 RP 与浮动 RP ,固定 RP 可由活动、比赛等的奖励获得,浮动 RP 由一般性通过题目获得。</p>
|
||||
<p>浮动 RP 是一个动态的数值,每个用户在每道题上获得的 RP 都不一样,并且未来也不固定(即浮动之意)。</p>
|
||||
<blockquote class="note">每道题的浮动 RP 由该题通过总人数与该用户通过此题的名次计算得来。</blockquote>
|
||||
</div>
|
||||
@@ -150,7 +151,7 @@
|
||||
</div>
|
||||
<div class="section__body typo">
|
||||
{{ _('no_translation_warn')|safe }}
|
||||
<p>Vijos 中题目的难度,根据递交数、通过率以及每个递交的递交时间和评测结果,通过算法计算得出。</p>
|
||||
<p>Hydro 中题目的难度,根据递交数、通过率以及每个递交的递交时间和评测结果,通过算法计算得出。</p>
|
||||
<p>因此,请注意以下几点:</p>
|
||||
<ul>
|
||||
<li>0. 一般地,难度的数值越大,该题目越难。</li>
|
||||
|
||||
@@ -17,22 +17,6 @@ const mapStateToProps = (state) => ({
|
||||
});
|
||||
|
||||
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() {
|
||||
dispatch({
|
||||
type: 'SCRATCHPAD_UI_SET_VISIBILITY',
|
||||
@@ -66,18 +50,6 @@ export default class ScratchpadPretestContainer extends React.PureComponent {
|
||||
<ScrollableInkTabBar
|
||||
extraContent={(
|
||||
<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
|
||||
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 isPretest = (data) => data.type === recordEnum.TYPE_PRETEST;
|
||||
const isPretest = (data) => data.pretest === true;
|
||||
|
||||
const getRecordDetail = (data) => {
|
||||
if (!shouldShowDetail(data)) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import _ from 'lodash';
|
||||
|
||||
const initialId = uuid();
|
||||
|
||||
@@ -23,67 +22,6 @@ export default function reducer(state = {
|
||||
},
|
||||
}, action) {
|
||||
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': {
|
||||
const { id, type, value } = action.payload;
|
||||
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