From 0d7d6ea12a56b658bcaca5bff9a4857be5f84737 Mon Sep 17 00:00:00 2001 From: masnn Date: Sat, 13 Jun 2020 13:33:37 +0800 Subject: [PATCH] testdata.convert --- hydro/entry/common.js | 2 +- hydro/handler/home.js | 6 +- hydro/handler/homework.js | 46 +++------ hydro/handler/misc.js | 10 +- hydro/handler/problem.js | 13 ++- hydro/handler/user.js | 4 +- hydro/lib/testdata.convert.ini.js | 17 ++++ hydro/model/problem.js | 23 +++-- hydro/model/record.js | 1 + hydro/model/token.js | 156 +++++++++++++++++------------- hydro/model/user.js | 2 +- hydro/service/server.js | 11 ++- templates/components/problem.html | 9 +- templates/homework_detail.html | 2 +- templates/problem_settings.html | 24 +++++ 15 files changed, 199 insertions(+), 127 deletions(-) create mode 100644 hydro/lib/testdata.convert.ini.js diff --git a/hydro/entry/common.js b/hydro/entry/common.js index 88f3acf51..0f80cd43b 100644 --- a/hydro/entry/common.js +++ b/hydro/entry/common.js @@ -5,7 +5,7 @@ const fs = require('fs'); const builtinLib = [ 'axios', 'download', 'i18n', 'mail', 'markdown', 'md5', 'misc', 'paginate', 'hash.hydro', 'rank', - 'template', 'validator', 'nav', 'sysinfo', + 'template', 'validator', 'nav', 'sysinfo', 'testdata.convert.ini', ]; const builtinModel = [ diff --git a/hydro/handler/home.js b/hydro/handler/home.js index 84041a048..28e913bbc 100644 --- a/hydro/handler/home.js +++ b/hydro/handler/home.js @@ -141,7 +141,7 @@ class HomeSecurityHandler extends Handler { for (const session of sessions) { if (tokenDigest === md5(session._id)) { // eslint-disable-next-line no-await-in-loop - await token.delete(session._id, token.TYPE_SESSION); + await token.del(session._id, token.TYPE_SESSION); return this.back(); } } @@ -149,7 +149,7 @@ class HomeSecurityHandler extends Handler { } async postDeleteAllTokens() { - await token.deleteByUid(this.user._id); + await token.delByUid(this.user._id); this.response.redirect = '/login'; } } @@ -200,7 +200,7 @@ class UserChangemailWithCodeHandler extends Handler { if (udoc) throw new UserAlreadyExistError(tdoc.mail); await Promise.all([ user.setEmail(this.user._id, tdoc.mail), - token.delete(code, token.TYPE_CHANGEMAIL), + token.del(code, token.TYPE_CHANGEMAIL), ]); this.response.redirect = '/home/security'; } diff --git a/hydro/handler/homework.js b/hydro/handler/homework.js index 4d5c63731..b1f26cd96 100644 --- a/hydro/handler/homework.js +++ b/hydro/handler/homework.js @@ -311,6 +311,18 @@ class HomeworkScoreboardHandler extends HomeworkHandler { } } +class HomeworkScoreboardDownloadHandler extends HomeworkHandler { + async get({ domainId, tid, ext }) { + const getContent = { + csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`, + html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }), + }; + if (!getContent[ext]) throw new ValidationError('ext'); + const [, rows] = await this.getScoreboard(domainId, tid, true, document.TYPE_CONTEST); + this.binary(await getContent[ext](rows), `${this.tdoc.title}.${ext}`); + } +} + async function apply() { contest.isHomeworkExtended = (tdoc) => { const now = new Date().getTime(); @@ -456,15 +468,15 @@ async function apply() { Route('homework_create', '/homework/create', HomeworkCreateHandler, PERM_CREATE_HOMEWORK); Route('homework_detail', '/homework/:tid', HomeworkDetailHandler, PERM_VIEW_HOMEWORK); Route('homework_scoreboard', '/homework/:tid/scoreboard', HomeworkScoreboardHandler, PERM_VIEW_HOMEWORK_SCOREBOARD); - Route('homework_problem', '/homework/:tid/p/:pid', HomeworkDetailProblemHandler, PERM_VIEW_HOMEWORK); - Route('homework_problem_submit', '/homework/:tid/p/:pid/submit', HomeworkDetailProblemSubmitHandler, PERM_SUBMIT_PROBLEM); + Route('homework_scoreboard', '/homework/:tid/scoreboard/download/:ext', HomeworkScoreboardDownloadHandler, PERM_VIEW_HOMEWORK_SCOREBOARD); + Route('homework_detail_problem', '/homework/:tid/p/:pid', HomeworkDetailProblemHandler, PERM_VIEW_HOMEWORK); + Route('homework_detail_problem_submit', '/homework/:tid/p/:pid/submit', HomeworkDetailProblemSubmitHandler, PERM_SUBMIT_PROBLEM); Route('homework_edit', '/homework/:tid/edit', HomeworkEditHandler); } global.Hydro.handler.homework = module.exports = apply; /* - @app.route('/homework/{tid:\w{24}}/code', 'homework_code') class HomeworkCodeHandler(base.OperationHandler): @base.limit_rate('homework_code', 3600, 60) @@ -490,32 +502,4 @@ class HomeworkCodeHandler(base.OperationHandler): await this.binary(output_buffer.getvalue(), 'application/zip', file_name='{}.zip'.format(tdoc['title'])) - - -@app.route('/homework/{tid}/scoreboard/download/{ext}', 'homework_scoreboard_download') -class HomeworkScoreboardDownloadHandler(contest.ContestMixin, base.Handler): - def _export_status_as_csv(this, rows): - # \r\n for notepad compatibility - csv_content = '\r\n'.join([','.join([str(c['value']) for c in row]) for row in rows]) - data = '\uFEFF' + csv_content - return data.encode() - - def _export_status_as_html(this, rows): - return this.render_html('contest_scoreboard_download_html.html', rows=rows).encode() - - @base.route_argument - @base.require_perm(builtin.PERM_VIEW_HOMEWORK) - @base.require_perm(builtin.PERM_VIEW_HOMEWORK_SCOREBOARD) - @base.sanitize - async def get(this, *, tid: objectid.ObjectId, ext: str): - get_status_content = { - 'csv': this._export_status_as_csv, - 'html': this._export_status_as_html, - } - if ext not in get_status_content: - raise error.ValidationError('ext') - tdoc, rows, udict = await this.get_scoreboard(document.TYPE_HOMEWORK, tid, True) - data = get_status_content[ext](rows) - file_name = tdoc['title'] - await this.binary(data, file_name='{}.{}'.format(file_name, ext)) */ diff --git a/hydro/handler/misc.js b/hydro/handler/misc.js index 8d60efd76..abb837269 100644 --- a/hydro/handler/misc.js +++ b/hydro/handler/misc.js @@ -9,10 +9,10 @@ const { Route, Handler } = require('../service/server'); const coll = db.collection('status'); class FileDownloadHandler extends Handler { - async get({ id, secret, name }) { + async get({ docId, secret, name }) { if (name) name = Buffer.from(name, 'base64').toString(); - this.response.attachment(name || id); - this.response.body = await file.get(id, secret); + this.response.attachment(name || docId); + this.response.body = await file.get(docId, secret); } } @@ -65,8 +65,8 @@ class MarkdownHandler extends Handler { } async function apply() { - Route('file_download', '/fs/:id/:secret', FileDownloadHandler); - Route('file_download_with_name', '/fs/:id/:name/:secret', FileDownloadHandler); + Route('file_download', '/fs/:docId/:secret', FileDownloadHandler); + Route('file_download_with_name', '/fs/:docId/:name/:secret', FileDownloadHandler); Route('status', '/status', StatusHandler); Route('status_update', '/status/update', StatusUpdateHandler); Route('switch_language', '/language/:lang', SwitchLanguageHandler); diff --git a/hydro/handler/problem.js b/hydro/handler/problem.js index c4a7ae521..60589d63e 100644 --- a/hydro/handler/problem.js +++ b/hydro/handler/problem.js @@ -247,8 +247,15 @@ class ProblemSettingsHandler extends ProblemManageHandler { ]; } - async post() { - // TODO(masnn) + async postConfig({ domainId, pid, yaml }) { + await problem.edit(domainId, pid, { config: yaml }); + this.back(); + } + + async postSetting({ + domainId, pid, hidden = false, category, tag, + }) { + await problem.edit(domainId, pid, { hidden, category, tag }); this.back(); } } @@ -303,7 +310,7 @@ class ProblemDataDownloadHandler extends ProblemDetailHandler { if (this.user._id !== this.pdoc.owner) this.checkPerm([PERM_READ_PROBLEM_DATA, PERM_JUDGE]); if (!this.pdoc.data) throw new ProblemDataNotFoundError(pid); else if (typeof this.pdoc.data === 'string') [, this.response.redirect] = this.pdoc.data.split('from:'); - this.response.redirect = file.url(this.pdoc.data, this.pdoc.title); + this.response.redirect = await file.url(this.pdoc.data, `${this.pdoc.title}.zip`); } } diff --git a/hydro/handler/user.js b/hydro/handler/user.js index 0283fe38b..a87f7edbb 100644 --- a/hydro/handler/user.js +++ b/hydro/handler/user.js @@ -89,7 +89,7 @@ class UserRegisterWithCodeHandler extends Handler { await user.create({ uid, uname, password, mail, regip: this.request.ip, }); - await token.delete(code, token.TYPE_REGISTRATION); + await token.del(code, token.TYPE_REGISTRATION); this.session.uid = uid; this.response.redirect = '/'; } @@ -129,7 +129,7 @@ class UserLostPassWithCodeHandler extends Handler { if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code); if (password !== verifyPassword) throw new VerifyPasswordError(); await user.setPassword(tdoc.uid, password); - await token.delete(code, token.TYPE_LOSTPASS); + await token.del(code, token.TYPE_LOSTPASS); this.response.redirect = '/'; } } diff --git a/hydro/lib/testdata.convert.ini.js b/hydro/lib/testdata.convert.ini.js new file mode 100644 index 000000000..83f8e357f --- /dev/null +++ b/hydro/lib/testdata.convert.ini.js @@ -0,0 +1,17 @@ +const yaml = require('js-yaml'); + +function convert(ini) { + const f = ini.split('\n'); + const count = f[0]; + const res = { cases: [] }; + for (let i = 1; i <= count; i++) { + const [input, output, time, score, memory] = f[i].split('|'); + const cur = { input, output, score }; + if (parseInt(time) !== 1) cur.time = `${time}s`; + if (!Number.isNaN(parseInt(memory))) cur.memory = parseInt(memory) / 1024; + res.cases.push(cur); + } + return yaml.safeDump(res); +} + +global.Hydro.lib['testdata.convert.ini'] = module.exports = convert; diff --git a/hydro/model/problem.js b/hydro/model/problem.js index b9724df42..a0257958a 100644 --- a/hydro/model/problem.js +++ b/hydro/model/problem.js @@ -90,7 +90,7 @@ function getMultiStatus(domainId, query) { * @returns {Pdoc} */ function edit(domainId, _id, $set) { - return document.set(domainId, document.TYPE_CONTEST, _id, $set); + return document.set(domainId, document.TYPE_PROBLEM, _id, $set); } function inc(domainId, _id, field, n) { @@ -111,13 +111,21 @@ async function random(domainId, query) { } async function getList(domainId, pids, doThrow = true) { + pids = new Set(pids); const r = {}; - if (doThrow) { - // eslint-disable-next-line no-await-in-loop - for (const pid of pids) r[pid] = await get(domainId, pid); - } else { - // eslint-disable-next-line no-await-in-loop - for (const pid of pids) r[pid] = await get(domainId, pid).catch((e) => e); + const pdocs = await document.getMulti( + domainId, document.TYPE_PROBLEM, + { $or: [{ docId: { $in: pids } }, { pid: { $in: pids } }] }, + ).toArray(); + for (const pdoc of pdocs) { + r[pdoc.docId] = r[pdoc.pid] = pdoc; + } + if (pdocs.length !== pids.size) { + if (doThrow) { + for (const pid of pids) { + if (!r[pid]) throw new ProblemNotFoundError(domainId, pid); + } + } } return r; } @@ -141,6 +149,7 @@ async function updateStatus(domainId, pid, uid, rid, status) { } async function setTestdata(domainId, _id, readStream) { + // TODO read config in zipfile const pdoc = await get(domainId, _id); const id = await file.add(readStream, 'data.zip'); if (pdoc.data && typeof pdoc.data === 'object') file.dec(this.pdoc.data); diff --git a/hydro/model/record.js b/hydro/model/record.js index 190c08bed..276f17ff6 100644 --- a/hydro/model/record.js +++ b/hydro/model/record.js @@ -110,6 +110,7 @@ async function judge(domainId, rid) { type: 'judge', rid, domainId, + config: pdoc.config || '', pid: rdoc.pid, data: pdoc.data, lang: rdoc.lang, diff --git a/hydro/model/token.js b/hydro/model/token.js index 86cac5e0d..5f7f6f2c8 100644 --- a/hydro/model/token.js +++ b/hydro/model/token.js @@ -3,85 +3,107 @@ const { ValidationError } = require('../error'); const coll = db.collection('token'); -function index() { +function ensureIndexes() { return Promise.all([ coll.createIndex([{ uid: 1 }, { tokenType: 1 }, { updateAt: -1 }], { sparse: true }), coll.createIndex('expireAt', { expireAfterSeconds: 0 }), ]); } +/** + * Add a token. + * @param {number} tokenType type of the token. + * @param {number} expireSeconds expire time, in seconds. + * @param {object} data extra data. + * @returns {Promise} token ID, token data + */ +async function add(tokenType, expireSeconds, data) { + const now = new Date(); + const str = String.random(32); + const res = await coll.insertOne({ + ...data, + _id: str, + tokenType, + createAt: now, + updateAt: now, + expireAt: new Date(now.getTime() + expireSeconds * 1000), + }); + return [str, res.ops]; +} + +/** + * Get a token. + * @param {string} tokenId token ID. + * @param {number} tokenType type of the token. + * @returns {Promise} The token document, or null. + */ +async function get(tokenId, tokenType) { + const res = await coll.findOne({ _id: tokenId, tokenType }); + if (!res) throw new ValidationError('token'); + return res; +} + +/** + * Update a token. + * @param {string} tokenId token ID. + * @param {number} tokenType type of the token. + * @param {number} expireSeconds expire time, in seconds. + * @param {object} data extra data. + * @returns {Promise} The token document, or null. + */ +async function update(tokenId, tokenType, expireSeconds, data) { + const now = new Date(); + const res = await coll.findOneAndUpdate({ _id: tokenId, tokenType }, { + $set: { + ...data, + updateAt: now, + expireAt: new Date(now.getTime() + expireSeconds * 1000), + tokenType, + }, + }); + return res.value; +} + +/** + * Delete a token. + * @param {string} tokenId token ID. + * @param {int} tokenType type of the token. + * @returns {Promise} true if deleted, or false. + */ +async function del(tokenId, tokenType) { + const result = await coll.deleteOne({ _id: tokenId, tokenType }); + return !!result.deletedCount; +} + +/** + * Update a token(or create one) + * @param {number} tokenType type of the token. + * @param {number} expireSeconds expire time, in seconds. + * @param {object} data extra data. + * @returns {Array} token ID, token data + */ +async function createOrUpdate(tokenType, expireSeconds, data) { + const d = await coll.findOne({ tokenType, ...data }); + if (!d) { + const res = await add(tokenType, expireSeconds, data); + return res[0]; + } + await update(d._id, tokenType, expireSeconds, data); + return d._id; +} + global.Hydro.model.token = module.exports = { TYPE_SESSION: 0, TYPE_CSRF_TOKEN: 1, TYPE_REGISTER: 2, TYPE_CHANGEMAIL: 3, - index, - - /** - * Add a token. - * @param {number} tokenType type of the token. - * @param {number} expireSeconds expire time, in seconds. - * @param {object} data extra data. - * @returns {Array} token ID, token data - */ - async add(tokenType, expireSeconds, data) { - const now = new Date(); - const str = String.random(32); - const res = await coll.insertOne({ - ...data, - _id: str, - tokenType, - createAt: now, - updateAt: now, - expireAt: new Date(now.getTime() + expireSeconds * 1000), - }); - return [str, res.ops]; - }, - - /** - * Get a token. - * @param {string} tokenId token ID. - * @param {number} tokenType type of the token. - * @returns {object} The token document, or null. - */ - async get(tokenId, tokenType) { - const res = await coll.findOne({ _id: tokenId, tokenType }); - if (!res) throw new ValidationError('token'); - return res; - }, - - /** - * Update a token. - * @param {string} tokenId token ID. - * @param {number} tokenType type of the token. - * @param {number} expireSeconds expire time, in seconds. - * @param {object} data extra data. - * @returns {object} The token document, or null. - */ - async update(tokenId, tokenType, expireSeconds, data) { - const now = new Date(); - const res = await coll.findOneAndUpdate({ _id: tokenId, tokenType }, { - $set: { - ...data, - updateAt: now, - expireAt: new Date(now.getTime() + expireSeconds * 1000), - tokenType, - }, - }); - return res.value; - }, - - /** - * Delete a token. - * @param {string} tokenId token ID. - * @param {int} tokenType type of the token. - * @returns {boolean} true if deleted, or false. - */ - async delete(tokenId, tokenType) { - const result = await coll.deleteOne({ _id: tokenId, tokenType }); - return !!result.deletedCount; - }, + ensureIndexes, + add, + createOrUpdate, + get, + update, + del, deleteByUid(uid) { return coll.deleteMany({ uid }); }, diff --git a/hydro/model/user.js b/hydro/model/user.js index 6b0e462c0..b3b871782 100644 --- a/hydro/model/user.js +++ b/hydro/model/user.js @@ -224,7 +224,7 @@ function deleteRoles(domainId, roles) { function ban(uid) { return Promise.all([ coll.updateOne({ _id: uid }, { $set: { ban: true } }), - token.deleteByUid(uid), + token.delByUid(uid), ]); } diff --git a/hydro/service/server.js b/hydro/service/server.js index 27b284e57..8ef00e9b4 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -54,6 +54,9 @@ const validate = { if (page <= 0) throw new ValidationError('page'); return page; }, + category: (category) => category.split(',').map((i) => i.trim()), + tag: (tag) => tag.split(',').map((i) => i.trim()), + hidden: (hidden) => !!hidden, duration: (duration) => { if (!Number.isNaN(parseFloat(duration))) duration = parseFloat(duration); if (duration <= 0) throw new ValidationError('duration'); @@ -76,6 +79,10 @@ const validate = { assert(typeof penaltyRules === 'object', new ValidationError('penalty_rules', 'invalid format')); return penaltyRules; }, + yaml: (input) => { + yaml.safeLoad(input); + return input; + }, }; async function prepare() { @@ -222,7 +229,7 @@ class Handler { if (bdoc) throw new BlacklistedError(this.request.ip); this.user = await user.getById(domainId, this.session.uid); if (!this.user) throw new UserNotFoundError(this.session.uid); - [this.csrfToken] = await token.add(token.TYPE_CSRF_TOKEN, 600, { + this.csrfToken = await token.createOrUpdate(token.TYPE_CSRF_TOKEN, 600, { path: this.request.path, uid: this.session.uid, }); @@ -400,7 +407,7 @@ class ConnectionHandler { async ___prepare({ domainId }) { try { this.session = await token.get(this.request.params.token, token.TYPE_CSRF_TOKEN); - await token.delete(this.request.params.token, token.TYPE_CSRF_TOKEN); + await token.del(this.request.params.token, token.TYPE_CSRF_TOKEN); } catch (e) { this.session = { uid: 1 }; } diff --git a/templates/components/problem.html b/templates/components/problem.html index 723c8a820..44d460365 100644 --- a/templates/components/problem.html +++ b/templates/components/problem.html @@ -1,9 +1,10 @@ -{% macro render_problem_title(pdoc, tdoc=none, prefix='/c', show_tags=true, show_invisible_flag=true, rp=none, invalid=false) %} +{% macro render_problem_title(pdoc, tdoc=none, show_tags=true, show_invisible_flag=true, rp=none, invalid=false) %} {%- if not invalid %} #{{ loop.index }}: - {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, prefix='/homework', show_invisible_flag=false, show_tags=false) }} + {{ problem.render_problem_title(pdict[pid], tdoc=tdoc, show_invisible_flag=false, show_tags=false) }} {% endfor %} diff --git a/templates/problem_settings.html b/templates/problem_settings.html index cd80c0e18..706003429 100644 --- a/templates/problem_settings.html +++ b/templates/problem_settings.html @@ -14,6 +14,29 @@ {% endif %} +
+
+

{{ _('Testdata Config') }}

+
+
+
+ {{ form.form_textarea({ + columns:null, + label:'Config', + name:'yaml', + value:pdoc['config']|default(''), + yaml:true + }) }} +
+ + + +
+
+
+

{{ _('Settings') }}

@@ -41,6 +64,7 @@ value:pdoc['tag']|join(', ') }) }}
+