添加功能:pretest discuss user dashboard statistics

This commit is contained in:
undefined committed 2020-04-27 12:18:36 +08:00
1 parent 61cba6f180
commit 15d43636c0
41 files changed
+920 -523

No files matched your search

+2 -1
View File
@@ -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
View File
@@ -1,9 +1,9 @@
node_modules/
config.yaml
.build/
.uibuild/
__*
ui/misc/.iconfont
ui/static/locale
.webpackStats.json
.cache
dist/
.webpackStats.json
+2 -1
View File
@@ -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
}
+11
View File
@@ -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);
+16
View File
@@ -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;
+5
View File
@@ -0,0 +1,5 @@
const path = require('path');
function root(name) {
return path.resolve(__dirname, '..', name);
}
module.exports = root;
+40
View File
@@ -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
View File
@@ -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');
require('./lib/i18n');
require('./utils');
const bus = require('./service/bus');
global.bus = new EventEmitter();
async function run() {
require('./utils');
require('./service/db');
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
View File
@@ -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
+259
View File
@@ -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);
+1 -1
View File
@@ -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);
+46 -20
View File
@@ -96,16 +96,29 @@ 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 = {
event: 'judge',
rid,
type: 0,
pid: rdoc.pid,
data: pdoc.data,
lang: rdoc.lang,
code: rdoc.code,
};
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,
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);
rid = await queue.get('judge', false); // eslint-disable-line no-await-in-loop
}
@@ -134,16 +147,29 @@ class JudgeConnectionHandler extends ConnectionHandler {
this.processing = null;
const rid = await queue.get('judge');
const rdoc = await record.get(rid);
const pdoc = await problem.getById(rdoc.pid);
const task = {
event: 'judge',
rid,
type: 0,
pid: rdoc.pid,
data: pdoc.data,
lang: rdoc.lang,
code: rdoc.code,
};
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,
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.processing = task.rid;
}
+112
View File
@@ -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
View File
@@ -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);
+1
View File
@@ -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 }) });
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+149
View File
@@ -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
View File
@@ -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
View File
@@ -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,
};
+4
View File
@@ -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
View File
@@ -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,
};
+1
View File
@@ -58,4 +58,5 @@ try {
} catch (e) {
console.error('Cannot load config');
}
module.exports = options;
+2 -1
View File
@@ -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
View File
@@ -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());
+3 -3
View File
@@ -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": [
@@ -46,4 +46,4 @@
"bin": {
"judger": "hydro/development.js"
}
}
}
+1 -1
View File
@@ -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 %}
+1 -1
View File
@@ -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 %}
+6 -1
View File
@@ -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>
+22 -4
View File
@@ -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
}) }}
+42 -42
View File
@@ -1,46 +1,46 @@
{% 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 %}
{{ 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 %}">
<div class="media">
<div class="media__left">
<div class="discussion__replies numbox">
<div class="numbox__num large">{{ ddoc['num_replies'] }}</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>
{% 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 %}">
<div class="media">
<div class="media__left">
<div class="discussion__replies numbox">
<div class="numbox__num large">{{ ddoc.nReply }}</div>
<div class="numbox__text">{{ _('Comments') }}</div>
</div>
</li>
{% endfor %}
</ol>
{% if page != undefined and dpcount != undefined %}
{{ paginator.render(page, dpcount) }}
{% endif %}
{% endif %}
</div>
<div class="media__body top">
<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="/discuss/{{ ddoc.parentType }}/{{ ddoc.parentId }}">
{% 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 %}
+8 -8
View File
@@ -1,10 +1,10 @@
<div data-fragment-id="problem_lucky">
<p>
<a href="/problem/random{% if category %}?category={{ category }}{% endif %}" class="expanded button" target="_blank">
<span class="icon icon-feeling-lucky"></span> {{ _('Lucky') }}
</a>
</p>
<div class="typo supplementary">
<p>{{ _('Pick a problem randomly based on current filter') }}</p>
</div>
<p>
<a href="/problem/random{% if category %}?category={{ category }}{% endif %}" class="expanded button" target="_blank">
<span class="icon icon-feeling-lucky"></span> {{ _('Lucky') }}
</a>
</p>
<div class="typo supplementary">
<p>{{ _('Pick a problem randomly based on current filter') }}</p>
</div>
</div>
+1 -1
View File
@@ -1,3 +1,3 @@
<div class="section__body" data-fragment-id="problem_stat">
<p>{{ _('{0} problems').format(pcount) }}</p>
<p>{{ _('{0} problems').format(pcount) }}</p>
</div>
+2 -2
View File
@@ -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]
}
]
+9 -8
View File
@@ -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 {
-16
View File
@@ -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;