模块化,添加webpack支持

This commit is contained in:
undefined committed 2020-04-29 19:08:03 +08:00
1 parent 4ec2a46877
commit 75ebc1a746
30 files changed
+622 -459

No files matched your search

+19 -10
View File
@@ -9,12 +9,16 @@ process.stdin.on('data', async (input) => {
}
});
global.Hydro = {};
require('./lib/i18n');
require('./utils');
const bus = require('./service/bus');
const loader = require('./lib/loader');
async function run() {
await loader.prepare();
await new Promise((resolve) => {
const h = () => {
console.log('Database connected');
@@ -27,16 +31,21 @@ async function run() {
require('./service/gridfs');
require('./service/queue');
const server = require('./service/server');
require('./handler/home');
require('./handler/problem');
require('./handler/record');
require('./handler/judge');
require('./handler/user');
require('./handler/contest');
require('./handler/training');
require('./handler/discussion');
require('./handler/manage');
require('./handler/import');
const HandlerHome = require('./handler/home');
const HandlerProblem = require('./handler/problem');
const HandlerRecord = require('./handler/record');
const HandlerJudge = require('./handler/judge');
const HandlerUser = require('./handler/user');
const HandlerContest = require('./handler/contest');
const HandlerTraining = require('./handler/training');
const HandlerDiscussion = require('./handler/discussion');
const HandlerManage = require('./handler/manage');
const HandlerImport = require('./handler/import');
await loader.model();
await loader.handler();
HandlerContest.apply();
HandlerDiscussion.apply();
HandlerImport.apply();
server.start();
}
run().catch((e) => {
+1 -1
View File
@@ -225,7 +225,7 @@ class DiscussionNotFoundError extends NotFoundError {
}
}
module.exports = {
global.Hydro.error = module.exports = {
BadRequestError,
BlacklistedError,
ForbiddenError,
+22 -8
View File
@@ -267,11 +267,25 @@ class ContestCreateHandler extends ContestHandler {
}
}
Route('/c', ContestListHandler);
Route('/c/:tid', ContestDetailHandler);
Route('/c/:tid/edit', ContestEditHandler);
Route('/c/:tid/scoreboard', ContestScoreboardHandler);
Route('/c/:tid/export/:ext', ContestScoreboardDownloadHandler);
Route('/c/:tid/p/:pid', ContestProblemHandler);
Route('/c/:tid/p/:pid/submit', ContestProblemSubmitHandler);
Route('/contest/create', ContestCreateHandler);
function apply() {
Route('/c', module.exports.ContestListHandler);
Route('/c/:tid', module.exports.ContestDetailHandler);
Route('/c/:tid/edit', module.exports.ContestEditHandler);
Route('/c/:tid/scoreboard', module.exports.ContestScoreboardHandler);
Route('/c/:tid/export/:ext', module.exports.ContestScoreboardDownloadHandler);
Route('/c/:tid/p/:pid', module.exports.ContestProblemHandler);
Route('/c/:tid/p/:pid/submit', module.exports.ContestProblemSubmitHandler);
Route('/contest/create', module.exports.ContestCreateHandler);
}
module.exports = {
ContestListHandler,
ContestDetailHandler,
ContestEditHandler,
ContestScoreboardHandler,
ContestScoreboardDownloadHandler,
ContestProblemHandler,
ContestProblemSubmitHandler,
ContestCreateHandler,
apply,
};
+22 -8
View File
@@ -257,11 +257,25 @@ class DiscussionEditHandler extends DiscussionHandler {
}
}
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);
function apply() {
Route('/discuss', module.exports.DiscussionMainHandler);
Route('/discuss/:did', module.exports.DiscussionDetailHandler);
Route('/discuss/:did/edit', module.exports.DiscussionEditHandler);
Route('/discuss/:did/raw', module.exports.DiscussionDetailRawHandler);
Route('/discuss/:did/:drid/raw', module.exports.DiscussionReplyRawHandler);
Route('/discuss/:did/:drid/:drrid/raw', module.exports.DiscussionTailReplyRawHandler);
Route('/discuss/:type/:docId', module.exports.DiscussionNodeHandler);
Route('/discuss/:type/:docId/create', module.exports.DiscussionCreateHandler);
}
module.exports = {
DiscussionMainHandler,
DiscussionDetailHandler,
DiscussionEditHandler,
DiscussionDetailRawHandler,
DiscussionReplyRawHandler,
DiscussionTailReplyRawHandler,
DiscussionNodeHandler,
DiscussionCreateHandler,
apply,
};
+228 -1
View File
@@ -1,8 +1,19 @@
const {
VerifyPasswordError, UserAlreadyExistError, InvalidTokenError,
NotFoundError,
} = require('../error');
const options = require('../options');
const { Route, Handler } = require('../service/server');
const md5 = require('../lib/md5');
const contest = require('../model/contest');
const user = require('../model/user');
const setting = require('../model/setting');
const token = require('../model/token');
const training = require('../model/training');
const { PERM_VIEW_TRAINING, PERM_VIEW_CONTEST, PERM_VIEW_DISCUSSION } = require('../permission');
const {
PERM_VIEW_TRAINING, PERM_VIEW_CONTEST, PERM_VIEW_DISCUSSION,
PERM_LOGGEDIN,
} = require('../permission');
const { CONTESTS_ON_MAIN, TRAININGS_ON_MAIN, DISCUSSIONS_ON_MAIN } = require('../options').constants;
class HomeHandler extends Handler {
@@ -57,4 +68,220 @@ class HomeHandler extends Handler {
}
}
class HomeSecurityHandler extends Handler {
async prepare() {
this.checkPerm(PERM_LOGGEDIN);
}
async get() {
// TODO(iceboy): pagination? or limit session count for uid?
const sessions = await token.getSessionListByUid(this.user._id);
const parsed = sessions.map((session) => ({
...session,
updateUa: useragent.parse(session.updateUa || session.createUa || ''),
updateGeoip: geoip.ip2geo(session.updateIp || session.createIp, this.user.viewLang),
_id: md5(session._id),
isCurrent: session._id === this.session._id,
}));
this.response.template = 'home_security.html';
this.response.body = { sessions: parsed };
}
async postChangePassword({ current, password, verifyPassword }) {
if (password !== verifyPassword) throw new VerifyPasswordError();
await user.changePassword(this.user._id, current, password);
this.back();
}
async postChangeMail({ currentPassword, mail }) {
this.limitRate('send_mail', 3600, 30);
this.user.checkPassword(currentPassword);
const udoc = await user.getByMail(mail);
if (udoc) throw new UserAlreadyExistError(mail);
const [rid] = await token.add(
token.TYPE_CHANGEMAIL,
options.changemail_token_expire_seconds,
{ uid: this.udoc._id, mail },
);
await mail.sendMail(mail, 'Change Email', 'user_changemail_mail.html', {
url: `/changeMail/${rid}`, uname: this.udoc.uname,
});
this.response.template = 'user_changemail_mail_sent.html';
}
async postDeleteToken({ tokenDigest }) {
const sessions = await token.getSessionListByUid(this.user._id);
for (const session in sessions) {
if (tokenDigest === md5(session._id)) {
await token.delete(session._id, token.TYPE_SESSION);
return this.back();
}
}
throw new InvalidTokenError(tokenDigest);
}
async postDeleteAllTokens() {
await token.deleteByUid(this.user._id);
this.back();
}
}
class HomeSettingsHandler extends Handler {
async prepare() {
this.checkPerm(PERM_LOGGEDIN);
}
async get({ category }) {
this.response.template = 'home_settings.html';
if (category === 'preference') {
this.response.body = {
category,
page_name: `home_${category}`,
settings: setting.PREFERENCE_SETTINGS,
};
} else if (category === 'account') {
this.response.body = {
category,
page_name: `home_${category}`,
settings: setting.ACCOUNT_SETTINGS,
};
} else throw new NotFoundError();
}
async post(args) {
// FIXME validation
await user.setById(this.user._id, args);
this.back();
}
}
class UserChangemailWithCodeHandler extends Handler {
async get({ code }) {
const tdoc = await token.get(code, token.TYPE_CHANGEMAIL);
if (!tdoc || tdoc.uid !== this.user._id) {
throw new InvalidTokenError(code);
}
const udoc = await user.getByEmail(tdoc.mail);
if (udoc) throw new UserAlreadyExistError(tdoc.mail);
// TODO(twd2): Ensure mail is unique.
await user.set_mail(this.user._id, tdoc.mail);
await token.delete(code, token.TYPE_CHANGEMAIL);
this.response.body = {};
this.response.redirect = '/home/security';
}
}
Route('/', HomeHandler);
Route('/home/security', HomeSecurityHandler);
Route('/home/changeMail/:code', UserChangemailWithCodeHandler);
Route('/home/settings/:category', HomeSettingsHandler);
/*
@app.route('/home/messages', 'home_messages', global_route=True)
class HomeMessagesHandler(base.OperationHandler):
def modify_udoc(this, udict, key):
udoc = udict.get(key)
if not udoc:
return
gravatar_url = misc.gravatar_url(udoc.get('gravatar'))
if 'gravatar' in udoc and udoc['gravatar']:
udict[key] = {**udoc,
'gravatar_url': gravatar_url,
'gravatar': ''}
@base.require_priv(builtin.PRIV_USER_PROFILE)
async def get(this):
# TODO(iceboy): projection, pagination.
mdocs = await message.get_multi(this.user['_id']).sort([('_id', -1)]).limit(50).to_list()
udict = await user.get_dict(
itertools.chain.from_iterable((mdoc['sender_uid'], mdoc['sendee_uid']) for mdoc in mdocs),
fields=user.PROJECTION_PUBLIC)
# TODO(twd2): improve here:
for mdoc in mdocs:
this.modify_udoc(udict, mdoc['sender_uid'])
this.modify_udoc(udict, mdoc['sendee_uid'])
this.json_or_render('home_messages.html', messages=mdocs, udict=udict)
@base.require_priv(builtin.PRIV_USER_PROFILE)
@base.require_csrf_token
@base.sanitize
async def post_send_message(this, *, uid: int, content: str):
udoc = await user.get_by_uid(uid, user.PROJECTION_PUBLIC)
if not udoc:
raise error.UserNotFoundError(uid)
mdoc = await message.add(this.user['_id'], udoc['_id'], content)
# TODO(twd2): improve here:
# projection
sender_udoc = await user.get_by_uid(this.user['_id'], user.PROJECTION_PUBLIC)
mdoc['sender_udoc'] = sender_udoc
this.modify_udoc(mdoc, 'sender_udoc')
mdoc['sendee_udoc'] = udoc
this.modify_udoc(mdoc, 'sendee_udoc')
if this.user['_id'] != uid:
await bus.publish('message_received-' + str(uid), {'type': 'new', 'data': mdoc})
this.json_or_redirect(this.url, mdoc=mdoc)
@base.require_priv(builtin.PRIV_USER_PROFILE)
@base.require_csrf_token
@base.sanitize
async def post_reply_message(this, *, message_id: objectid.ObjectId, content: str):
mdoc, reply = await message.add_reply(message_id, this.user['_id'], content)
if not mdoc:
return error.MessageNotFoundError(message_id)
if mdoc['sender_uid'] != mdoc['sendee_uid']:
if mdoc['sender_uid'] == this.user['_id']:
other_uid = mdoc['sendee_uid']
else:
other_uid = mdoc['sender_uid']
mdoc['reply'] = [reply]
await bus.publish('message_received-' + str(other_uid), {'type': 'reply', 'data': mdoc})
this.json_or_redirect(this.url, reply=reply)
@base.require_priv(builtin.PRIV_USER_PROFILE)
@base.require_csrf_token
@base.sanitize
async def post_delete_message(this, *, message_id: objectid.ObjectId):
await message.delete(message_id, this.user['_id'])
this.back();
@app.connection_route('/home/messages-conn', 'home_messages-conn', global_route=True)
class HomeMessagesConnection(base.Connection):
@base.require_priv(builtin.PRIV_USER_PROFILE)
async def on_open(this):
await super(HomeMessagesConnection, this).on_open()
bus.subscribe(this.on_message_received, ['message_received-' + str(this.user['_id'])])
async def on_message_received(this, e):
this.send(**e['value'])
async def on_close(this):
bus.unsubscribe(this.on_message_received)
@app.route('/home/file', 'home_file', global_route=True)
class HomeFileHandler(base.OperationHandler):
def file_url(this, fdoc):
return options.cdn_prefix.rstrip('/') + \
this.reverse_url('fs_get', domain_id=builtin.DOMAIN_ID_SYSTEM,
secret=fdoc['metadata']['secret'])
@base.require_priv(builtin.PRIV_USER_PROFILE)
async def get(this):
ufdocs = await userfile.get_multi(owner_uid=this.user['_id']).to_list()
fdict = await fs.get_meta_dict(ufdoc.get('file_id') for ufdoc in ufdocs)
this.render('home_file.html', ufdocs=ufdocs, fdict=fdict)
@base.require_priv(builtin.PRIV_USER_PROFILE)
@base.post_argument
@base.require_csrf_token
@base.sanitize
async def post_delete(this, *, ufid: document.convert_doc_id):
ufdoc = await userfile.get(ufid)
if not this.own(ufdoc, priv=builtin.PRIV_DELETE_FILE_this):
this.check_priv(builtin.PRIV_DELETE_FILE)
result = await userfile.delete(ufdoc['doc_id'])
if result:
await userfile.dec_usage(this.user['_id'], ufdoc['length'])
this.redirect(this.referer_or_main)
*/
+9 -50
View File
@@ -1,57 +1,10 @@
const assert = require('assert');
const download = require('../lib/download');
const problem = require('../model/problem');
const { Route, Handler } = require('../service/server');
const { PERM_CREATE_PROBLEM } = require('../permission');
const axios = require('../lib/axios');
const { ValidationError, RemoteOnlineJudgeError } = require('../error');
const { ValidationError } = require('../error');
class ProblemImportHandler extends Handler {
async syzoj(url) {
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);
const res = await axios.get(`${url}export`);
assert(res.status === 200, new RemoteOnlineJudgeError('Cannot connect to target server'));
assert(res.data.success, new RemoteOnlineJudgeError((res.data.error || {}).message));
const p = res.data.obj;
const content = [
this.translate('problem.import.problem_description'),
p.description,
this.translate('problem.import.input_format'),
p.input_format,
this.translate('problem.import.output_format'),
p.output_format,
this.translate('problem.import.hint'),
p.hint,
this.translate('problem.import.limit_and_hint'),
p.limit_and_hint,
];
if (p.have_additional_file) {
content.push(
this.translate('problem.import.additional_file'),
`${url}download/additional_file`,
);
}
const pdoc = {
title: p.title,
content: content.join(' \n'),
owner: this.user._id,
from: url,
pid: `${host}_${pid}`,
config: {
time: p.time_limit,
memory: p.memory_limit * 1024,
filename: p.file_io_input_name,
type: p.type,
tags: p.tags,
},
};
const r = await download(`${url}testdata/download`);
return [pdoc, r];
}
async prepare() {
this.checkPerm(PERM_CREATE_PROBLEM);
}
@@ -82,4 +35,10 @@ class ProblemImportHandler extends Handler {
}
}
Route('/problem/import', ProblemImportHandler);
async function apply() {
Route('/problem/import', module.exports.ProblemImportHandler);
}
global.Hydro['handler.import'] = module.exports = {
ProblemImportHandler, apply,
};
+1 -1
View File
@@ -20,4 +20,4 @@ async function post(url, data, options) {
return res;
}
module.exports = { get, post };
global.Hydro['lib.axios'] = module.exports = { get, post };
+2 -1
View File
@@ -19,4 +19,5 @@ async function download(url, path, retry = 3) {
}
return r;
}
module.exports = download;
global.Hydro['lib.download'] = module.exports = download;
+43
View File
@@ -0,0 +1,43 @@
const fs = require('fs');
const path = require('path');
let installed;
function root(name) {
return path.resolve(process.cwd(), name);
}
function exist(name) {
try {
fs.statSync(root(name));
} catch (e) {
return false;
}
return true;
}
const superRequire = (name) => (__non_webpack_require__
? __non_webpack_require__(root(name))
: require(root(name)));
async function prepare() {
installed = fs.readdirSync(root('.build/module'));
}
async function handler() {
for (const i of installed) {
if (exist(`.build/module/${i}/handler.js`)) {
superRequire(`.build/module/${i}/handler.js`);
}
console.log(`Handler init: ${i}`);
}
}
async function model() {
for (const i of installed) {
if (exist(`.build/module/${i}/model.js`)) {
superRequire(`.build/module/${i}/model.js`);
}
console.log(`Model init: ${i}`);
}
}
global.Hydro['lib.loader'] = module.exports = { prepare, handler, model };
+4 -2
View File
@@ -1,4 +1,4 @@
module.exports = function* ranked(diter, equ = (a, b) => a === b) {
function* ranked(diter, equ = (a, b) => a === b) {
let last = null;
let r = 0;
let count = 0;
@@ -8,4 +8,6 @@ module.exports = function* ranked(diter, equ = (a, b) => a === b) {
last = doc;
yield [r, doc];
}
};
}
global.Hydro['lib.rank'] = module.exports = ranked;
+3 -5
View File
@@ -11,11 +11,7 @@ const db = require('../service/db');
const coll = db.collection('contest');
const collStatus = db.collection('contest.status');
const RULES = {
homework: require('../module/contest/homework'),
oi: require('../module/contest/oi'),
acm: require('../module/contest/acm'),
};
const RULES = {};
/**
* @typedef {import('bson').ObjectID} ObjectID
@@ -197,6 +193,8 @@ module.exports = {
: 'finished'),
};
global.Hydro['model.contest'] = module.exports;
/*
def _get_status_journal(tsdoc):
+60
View File
@@ -0,0 +1,60 @@
const builtin = require('./builtin');
const options = require('../options');
const i18n = require('../lib/i18n');
const Setting = (
family, key, range = null,
value = null, ui = 'text', name = '',
desc = '', imageClass = '',
) => ({
family, key, range, value, ui, name, desc, imageClass,
});
const PREFERENCE_SETTINGS = [
Setting('setting_display', 'view_lang', i18n,
options.default_locale, 'select', 'UI Language'),
Setting('setting_display', 'timezone', [], // TODO(masnn) timezone
'Asia/Shanghai', 'select', 'Timezone'),
Setting('setting_usage', 'code_lang', builtin.LANG_TEXTS,
null, 'select', 'Default Code Language'),
Setting('setting_usage', 'code_template', null,
null, 'textarea', 'Default Code Template',
'If left blank, the built-in template of the corresponding language will be used.'),
];
const ACCOUNT_SETTINGS = [
Setting('setting_info', 'gravatar', null,
null, null, 'Gravatar Email',
'We use <a href="https://en.gravatar.com/" target="_blank">Gravatar</a> to present your avatar icon.'),
Setting('setting_info', 'qq', null,
null, null, 'QQ'),
Setting('setting_info', 'wechat', null,
null, null, 'WeChat'),
Setting('setting_info', 'gender', builtin.USER_GENDER_RANGE,
null, 'select', 'Gender'),
Setting('setting_info', 'bio', null,
null, 'markdown', 'Bio'),
Setting('setting_privacy', 'show_mail', builtin.PRIVACY_RANGE,
null, 'select', 'Email Visibility'),
Setting('setting_privacy', 'show_qq', builtin.PRIVACY_RANGE,
null, 'select', 'QQ Visibility'),
Setting('setting_privacy', 'show_wechat', builtin.PRIVACY_RANGE,
null, 'select', 'WeChat Visibility'),
Setting('setting_privacy', 'show_gender', builtin.PRIVACY_RANGE,
null, 'select', 'Gender Visibility'),
Setting('setting_privacy', 'show_bio', builtin.PRIVACY_RANGE,
null, 'select', 'Bio Visibility'),
Setting('setting_customize', 'background_img', builtin.BACKGROUND_RANGE,
null, 'image_radio', 'Profile Background Image',
'Choose the background image in your profile page.',
'user-profile-bg--thumbnail-{0}'),
];
const SETTINGS = [...PREFERENCE_SETTINGS, ...ACCOUNT_SETTINGS];
const SETTINGS_BY_KEY = {};
for (const setting in SETTINGS) SETTINGS_BY_KEY[setting.key] = setting;
module.exports = {
PREFERENCE_SETTINGS, ACCOUNT_SETTINGS, SETTINGS, SETTINGS_BY_KEY,
};
+8
View File
@@ -7,6 +7,8 @@ module.exports = {
TYPE_SESSION: 0,
TYPE_CSRF_TOKEN: 1,
TYPE_REGISTER: 2,
TYPE_CHANGEMAIL: 3,
/**
* Add a token.
* @param {number} tokenType type of the token.
@@ -71,7 +73,13 @@ module.exports = {
const result = await coll.deleteOne({ _id: tokenId, tokenType });
return !!result.deletedCount;
},
deleteByUid(uid) {
return coll.deleteMany({ uid });
},
getMostRecentSessionByUid(uid) {
return coll.findOne({ uid, token_type: this.TYPE_SESSION }, { sort: { updateAt: -1 } });
},
getSessionListByUid(uid) {
return coll.find({ uid, token_type: this.TYPE_SESSION }).sort('updateAt', -1).toArray();
},
};
-111
View File
@@ -1,111 +0,0 @@
module.exports = {
TEXT: 'ACM/ICPC',
check: () => { },
showScoreboard: () => true,
showRecord: (tdoc, now) => now > tdoc.endAt,
stat: (tdoc, journal) => {
const naccept = {};
const effective = {};
const detail = [];
let accept = 0;
let time = 0;
for (const j in journal) {
if (tdoc.pids.includes(j.pid)
&& !(effective.includes(j.pid) && effective[j.pid].accept)) {
effective[j.pid] = j;
}
if (!j.accept) naccept[j.pid]++;
}
function _time(jdoc) {
const real = jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000);
const penalty = 20 * 60 * naccept[jdoc.pid];
return real + penalty;
}
for (const j of effective) detail.push({ ...j, naccept: naccept[j.pid], time: _time(j) });
for (const d of detail) {
accept += d.accept;
if (d.accept) time += d.time;
}
return { accept, time, detail };
},
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
const columns = [
{ type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') },
{ type: 'solved_problems', value: _('Solved Problems') },
];
if (isExport) {
columns.push({ type: 'total_time', value: _('Total Time (Seconds)') });
columns.push({ type: 'total_time_str', value: _('Total Time') });
}
for (const i in tdoc.pids) {
if (isExport) {
columns.push({
type: 'problem_flag',
value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title),
});
columns.push({
type: 'problem_time',
value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')),
});
columns.push({
type: 'problem_time_str',
value: '#{0} {1}'.format(i + 1, _('Time')),
});
} else {
columns.push({
type: 'problem_detail',
value: '#{0}'.format(i + 1),
raw: pdict[tdoc.pids[i]],
});
}
}
const rows = [columns];
for (const [rank, tsdoc] of rankedTsdocs) {
const tsddict = {};
if (tdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; }
const row = [];
row.push(
{ type: 'string', value: rank },
{ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] },
{ type: 'string', value: tsdoc.accept || 0 },
);
if (isExport) {
row.push(
{ type: 'string', value: tsdoc.time || 0.0 },
{ type: 'string', value: tsdoc.time || 0.0 },
);
}
for (const pid of tdoc.pids) {
let rdoc;
let colAccepted;
let colTime;
let colTimeStr;
if ((tsddict[pid] || {}).accept) {
rdoc = tsddict[pid].rid;
colAccepted = _('Accepted');
colTime = tsddict[pid].time;
colTimeStr = colTime;
} else {
rdoc = null;
colAccepted = '-';
colTime = '-';
colTimeStr = '-';
}
if (isExport) {
row.push({ type: 'string', value: colAccepted });
row.push({ type: 'string', value: colTime });
row.push({ type: 'string', value: colTimeStr });
} else {
row.push({
type: 'record',
value: '{0}\n{1}'.format(colAccepted, colTimeStr),
raw: rdoc,
});
}
rows.push(row);
}
}
return rows;
},
};
-95
View File
@@ -1,95 +0,0 @@
module.exports = {
TEXT: 'Assignment',
check() {},
stat(tdoc, journal) {
const effective = {};
const detail = [];
let score = 0;
let time = 0;
for (const j in journal) {
if (tdoc.pids.includes(j.pid)
&& !(effective.includes(j.pid) && effective[j.pid].accept)) {
effective[j.pid] = j;
}
}
const _time = (jdoc) => jdoc.rid.generationTime - Math.floor(tdoc.beginAt / 1000);
for (const j in effective) {
detail.push({
...effective[j],
time: _time(effective[j]),
});
}
for (const d of detail) {
score += d.score;
time += d.time;
}
return {
score, time, detail,
};
},
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
const columns = [
{ type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') },
{ type: 'display_name', value: _('Display Name') },
{ type: 'total_score', value: _('Score') },
];
if (isExport) {
columns.push(
{ type: 'total_original_score', value: _('Original Score') },
{ type: 'total_time', value: _('Total Time (Seconds)') },
);
}
columns.push({ type: 'total_time_str', value: _('Total Time') });
for (const i in tdoc.pids) {
if (isExport) {
columns.push(
{ type: 'problem_score', value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title) },
{ type: 'problem_original_score', value: '#{0} {1}'.format(i + 1, _('Original Score')) },
{ type: 'problem_time', value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')) },
{ type: 'problem_time_str', value: '#{0} {1}'.format(i + 1, _('Time')) },
);
} else columns.push({ type: 'problem_detail', value: '#{0}'.format(i + 1), raw: pdict[tdoc.pids[i]] });
}
const rows = [columns];
for (const [rank, tsdoc] in rankedTsdocs) {
const tsddict = {};
if (tsdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; }
const row = [
{ type: 'string', value: rank },
{ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] },
{ type: 'string', value: tsdoc.penalty_score || 0 },
];
if (isExport) {
row.push(
{ type: 'string', value: tsdoc.score || 0 },
{ type: 'string', value: tsdoc.time || 0.0 },
);
}
row.push({ type: 'string', value: tsdoc.time || 0 });
for (const pid of tdoc.pids) {
const rdoc = (tsddict[pid] || {}).rid || null;
const colScore = (tsddict[pid] || {}).penalty_score || '-';
const colOriginalScore = (tsddict[pid] || {}).score || '-';
const colTime = (tsddict[pid] || {}).time || '-';
const colTimeStr = colTime !== '-' ? colTime : '-';
if (isExport) {
row.push(
{ type: 'string', value: colScore },
{ type: 'string', value: colOriginalScore },
{ type: 'string', value: colTime },
{ type: 'string', value: colTimeStr },
);
} else {
row.push({
type: 'record',
value: '{0} / {1}\n{2}'.format(colScore, colOriginalScore, colTimeStr),
raw: rdoc,
});
}
}
rows.push(row);
}
return rows;
},
};
-63
View File
@@ -1,63 +0,0 @@
const ranked = require('../../lib/rank');
module.exports = {
TEXT: 'OI',
check: () => { },
stat: (tdoc, journal) => {
const detail = {};
let score = 0;
for (const j in journal) {
if (tdoc.pids.includes(j.pid)) {
detail[j.pid] = j;
score += j.score;
}
}
return { score, detail };
},
showScoreboard(tdoc, now) {
return now > tdoc.endAt;
},
showRecord(tdoc, now) {
return now > tdoc.endAt;
},
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
const columns = [
{ type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') },
{ type: 'total_score', value: _('Total Score') },
];
for (const i in tdoc.pids) {
if (isExport) {
columns.push({
type: 'problem_score',
value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title),
});
} else {
columns.push({
type: 'problem_detail',
value: '#{0}'.format(i + 1),
raw: pdict[tdoc.pids[i]],
});
}
}
const rows = [columns];
for (const [rank, tsdoc] of rankedTsdocs) {
const tsddict = {};
if (tsdoc.journal) { for (const item of tsdoc.journal) tsddict[item.pid] = item; }
const row = [];
row.push({ type: 'string', value: rank });
row.push({ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] });
row.push({ type: 'string', value: tsdoc.score || 0 });
for (const pid of tdoc.pids) {
row.push({
type: 'record',
value: (tsddict[pid] || {}).score || '-',
raw: (tsddict[pid] || {}).rid || null,
});
}
rows.push(row);
}
return rows;
},
rank: (tdocs) => ranked(tdocs, (a, b) => a.score === b.score),
};
+1 -1
View File
@@ -13,7 +13,7 @@ let options = {
password: '',
},
template: {
path: path.join(__dirname, '..', 'templates'),
path: path.join(process.cwd(), 'templates'),
},
listen: {
host: '127.0.0.1',
+3 -1
View File
@@ -240,7 +240,9 @@ class Handler {
console.error(error.message, error.params);
console.error(error.stack);
this.response.template = error instanceof UserFacingError ? 'error.html' : 'bsod.html';
this.response.body = { error: { message: error.message, params: error.params, stack: error.stack } };
this.response.body = {
error: { message: error.message, params: error.params, stack: error.stack },
};
await this.___cleanup().catch(() => { });
}
}