From bc50913e6e2e06e6c98da84d9d4713486d5f01f7 Mon Sep 17 00:00:00 2001 From: masnn Date: Tue, 2 Jun 2020 11:33:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=9C=E4=B8=9A=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 +- build/index.js | 2 - build/template.js | 2 +- extensions/homework/handler.js | 0 extensions/homework/hydro.json | 5 + hydro/handler/contest.js | 20 +- hydro/handler/fs.js | 20 - hydro/handler/home.js | 2 +- hydro/handler/homework.js | 483 ++++++++++++++++++ hydro/handler/{status.js => misc.js} | 21 +- hydro/handler/user.js | 19 +- hydro/lib/nav.js | 1 + hydro/lib/template.js | 29 +- hydro/loader.js | 2 +- hydro/model/contest.js | 94 ++-- hydro/model/record.js | 16 +- hydro/model/user.js | 1 + hydro/permission.js | 11 +- hydro/service/server.js | 87 +++- hydro/utils.js | 17 + templates/components/homework.html | 8 + templates/contest_detail.html | 1 - templates/contest_main.html | 1 - templates/contest_scoreboard.html | 1 - templates/discussion_create.html | 1 - templates/discussion_detail.html | 1 - templates/error.html | 1 - templates/home_messages.html | 1 - templates/home_security.html | 1 - templates/homework_detail.html | 77 +++ templates/homework_edit.html | 105 ++++ templates/homework_main.html | 68 +++ templates/manage_dashboard.html | 1 - templates/manage_edit.html | 1 - templates/manage_module.html | 1 - templates/manage_permission.html | 1 - templates/manage_role.html | 1 - templates/manage_settings.html | 1 - templates/manage_user.html | 1 - .../homework_default_penalty_rules.yaml | 6 + templates/partials/homework_sidebar.html | 85 +++ templates/problem_detail.html | 1 - templates/problem_import.html | 1 - templates/problem_main.html | 1 - templates/problem_settings.html | 1 - templates/problem_solution.html | 1 - templates/problem_submit.html | 1 - templates/record_detail.html | 1 - templates/record_main.html | 1 - templates/status.html | 59 ++- templates/training_detail.html | 1 - templates/training_main.html | 1 - templates/user_detail.html | 1 - templates/user_login.html | 1 - templates/user_register.html | 1 - templates/user_register_with_code.html | 1 - ui/pages/homework_detail.page.styl | 13 + ui/pages/homework_main.page.js | 37 ++ ui/pages/homework_main.page.styl | 14 + ui/pages/homework_scoreboard.page.styl | 22 + 60 files changed, 1161 insertions(+), 197 deletions(-) create mode 100644 extensions/homework/handler.js create mode 100644 extensions/homework/hydro.json delete mode 100644 hydro/handler/fs.js create mode 100644 hydro/handler/homework.js rename hydro/handler/{status.js => misc.js} (65%) create mode 100644 templates/components/homework.html create mode 100644 templates/homework_detail.html create mode 100644 templates/homework_edit.html create mode 100644 templates/homework_main.html create mode 100644 templates/partials/homework_default_penalty_rules.yaml create mode 100644 templates/partials/homework_sidebar.html create mode 100644 ui/pages/homework_detail.page.styl create mode 100644 ui/pages/homework_main.page.js create mode 100644 ui/pages/homework_main.page.styl create mode 100644 ui/pages/homework_scoreboard.page.styl diff --git a/.vscode/settings.json b/.vscode/settings.json index 4ca171c0d..e40aaf21e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,6 @@ "files.eol": "\n", "editor.detectIndentation": false, "editor.tabSize": 4, - "editor.formatOnSave": true + "editor.formatOnSave": true, + "editor.renderWhitespace": "boundary" } \ No newline at end of file diff --git a/build/index.js b/build/index.js index 692c3cd89..91d99ee5d 100644 --- a/build/index.js +++ b/build/index.js @@ -31,8 +31,6 @@ async function build(type) { lang[i.split('.')[0]] = yaml.safeLoad(content); } const exclude = [ - 'partials/training_default.json', - 'partials/problem_default.md', 'bsod.html', ]; const builtin = { diff --git a/build/template.js b/build/template.js index 4a0f2424e..30324f2a2 100644 --- a/build/template.js +++ b/build/template.js @@ -25,7 +25,7 @@ const build = (dirOrObject, exclude = []) => { } } else templates = dirOrObject; for (const i in templates) { - if (!exclude.includes(i)) { + if (!exclude.includes(i) && i.endsWith('.html')) { templates[i] = templates[i] .trim() .replace(/ *\n */gmi, ' ') diff --git a/extensions/homework/handler.js b/extensions/homework/handler.js new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/homework/hydro.json b/extensions/homework/hydro.json new file mode 100644 index 000000000..3a5c14838 --- /dev/null +++ b/extensions/homework/hydro.json @@ -0,0 +1,5 @@ +{ + "id": "homework", + "version": "1.0.0", + "description": "Homework support" +} \ No newline at end of file diff --git a/hydro/handler/contest.js b/hydro/handler/contest.js index 29116e155..8d7f4a8bb 100644 --- a/hydro/handler/contest.js +++ b/hydro/handler/contest.js @@ -209,8 +209,10 @@ class ContestProblemSubmitHandler extends ContestProblemHandler { async get({ domainId, tid, pid }) { let rdocs = []; if (this.canShowRecord(this.tdoc)) { - rdocs = await record.getUserInProblemMulti(domainId, this.user._id, this.pdoc.docId, true) - .sort({ _id: -1 }).limit(10).toArray(); + rdocs = await record.getUserInProblemMulti( + domainId, this.user._id, + this.pdoc.docId, true, + ).sort({ _id: -1 }).limit(10).toArray(); } this.response.template = 'problem_submit.html'; const path = [ @@ -258,10 +260,6 @@ class ContestProblemSubmitHandler extends ContestProblemHandler { } class ContestCreateHandler extends ContestHandler { - async prepare() { - this.checkPerm(PERM_CREATE_CONTEST); - } - async get() { this.response.template = 'contest_edit.html'; const rules = {}; @@ -294,10 +292,12 @@ class ContestCreateHandler extends ContestHandler { } catch (e) { throw new ValidationError('beginAtDate', 'beginAtTime'); } - const endAt = new Date(beginAt.getTime() + duration * 3600 * 1000); - if (beginAt >= endAt) throw new ValidationError('duration'); + const endAt = beginAt.delta({ hour: duration }); pids = await this.verifyProblems(domainId, pids); - const tid = await contest.add(domainId, title, content, this.user._id, rule, beginAt, endAt, pids); + const tid = await contest.add( + domainId, title, content, + this.user._id, rule, beginAt, endAt, pids, + ); this.response.body = { tid }; this.response.redirect = `/c/${tid}`; } @@ -311,7 +311,7 @@ async function apply() { 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); + Route('/contest/create', module.exports.ContestCreateHandler, PERM_CREATE_CONTEST); } global.Hydro.handler.contest = module.exports = { diff --git a/hydro/handler/fs.js b/hydro/handler/fs.js deleted file mode 100644 index 9815ad93d..000000000 --- a/hydro/handler/fs.js +++ /dev/null @@ -1,20 +0,0 @@ -const file = require('../model/file'); -const { Route, Handler } = require('../service/server'); - -class FileDownloadHandler extends Handler { - async get({ id, secret, name }) { - if (name) name = Buffer.from(name, 'base64').toString(); - this.response.attachment(name || id); - this.response.body = await file.get(id, secret); - } -} - -async function apply() { - Route('/fs/:id/:secret', module.exports.FileDownloadHandler); - Route('/fs/:id/:name/:secret', module.exports.FileDownloadHandler); -} - -global.Hydro.handler.file = module.exports = { - FileDownloadHandler, - apply, -}; diff --git a/hydro/handler/home.js b/hydro/handler/home.js index 0dc2482d1..aaeb548eb 100644 --- a/hydro/handler/home.js +++ b/hydro/handler/home.js @@ -280,7 +280,7 @@ class HomeFileHandler(base.OperationHandler): @base.require_priv(builtin.PRIV_USER_PROFILE) @base.post_argument - @base.require_csrf_token + @base.require_csrfToken @base.sanitize async def post_delete(this, *, ufid: document.convert_doc_id): ufdoc = await userfile.get(ufid) diff --git a/hydro/handler/homework.js b/hydro/handler/homework.js new file mode 100644 index 000000000..4a5560cf5 --- /dev/null +++ b/hydro/handler/homework.js @@ -0,0 +1,483 @@ +const { + ValidationError, HomeworkNotLiveError, ProblemNotFoundError, + HomeworkNotAttendedError, +} = require('../error'); +const { + PERM_VIEW_HOMEWORK, PERM_ATTEND_HOMEWORK, PERM_VIEW_PROBLEM, + PERM_SUBMIT_PROBLEM, PERM_CREATE_HOMEWORK, PERM_EDIT_HOMEWORK, +} = require('../permission'); +const { Route, Handler } = require('../service/server'); +const system = require('../model/system'); +const user = require('../model/user'); +const contest = require('../model/contest'); +const discussion = require('../model/discussion'); +const problem = require('../model/problem'); +const record = require('../model/record'); +const document = require('../model/document'); +const paginate = require('../lib/paginate'); + +function _formatPenaltyRules(penaltyRules) { + let doc = ''; + for (const time in penaltyRules) { + const coefficient = penaltyRules[time]; + const parsedTime = Math.round(parseFloat(time) / 36) / 100; + doc += '{0}: {1}\n'.format(parsedTime, coefficient); + } + return doc; +} + +const HomeworkHandler = contest.ContestHandlerMixin(Handler); + +class HomeworkMainHandler extends HomeworkHandler { + async get({ domainId }) { + const tdocs = await contest.getMulti(domainId, {}, document.TYPE_HOMEWORK).toArray(); + const calendar = []; + for (const tdoc of tdocs) { + console.log(tdoc); + const cal = { ...tdoc }; + if (contest.isHomeWorkExtended(tdoc) || contest.isDone(tdoc)) { + cal.endAt = tdoc.endAt; + cal.penaltySince = tdoc.penaltySince; + } else { + cal.endAt = tdoc.penaltySince; + } + calendar.push(cal); + } + this.response.body = { tdocs, calendar }; + this.response.template = 'homework_main.html'; + } +} + +class HomeworkDetailHandler extends HomeworkHandler { + async get({ domainId, tid, page = 1 }) { + const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); + const [tsdoc, pdict] = await Promise.all([ + contest.getStatus(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK), + problem.getList(domainId, tdoc.pids), + ]); + const psdict = {}; + let rdict = {}; + let attended = false; + if (tsdoc) { + attended = tsdoc.attend === 1; + for (const pdetail of tsdoc.detail || []) { + psdict[pdetail.pid] = pdetail; + rdict[pdetail.rid] = { _id: pdetail.rid }; + } + if (this.canShowRecord(tdoc) && tsdoc.detail) { + rdict = await record.getList( + domainId, + tsdoc.detail.map((pdetail) => pdetail.rid), + true, + ); + } + } + // discussion + const [ddocs, dpcount, dcount] = await paginate( + discussion.getMulti(domainId, { parentType: tdoc.docType, parentId: tdoc.docId }), + page, + await system.get('DISCUSSION_PER_PAGE'), + ); + const uids = ddocs.map((ddoc) => ddoc.owner); + uids.push(tdoc.owner); + const udict = await user.getList(domainId, uids); + const path = [ + ['homework_main', '/homework'], + [tdoc.title, null, true], + ]; + this.response.template = 'homework_detail.html'; + this.response.body = { + tdoc, tsdoc, attended, udict, pdict, psdict, rdict, ddocs, page, dpcount, dcount, path, + }; + } + + async postAttend({ domainId, tid }) { + this.checkPerm(PERM_ATTEND_HOMEWORK); + const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); + if (contest.isDone(tdoc)) { + throw new HomeworkNotLiveError(tdoc.docId); + } + await contest.attend(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK); + this.back(); + } +} + +class HomeworkDetailProblemHandler extends HomeworkHandler { + async prepare({ domainId, tid, pid }) { + this.checkPerm(PERM_VIEW_PROBLEM); + [this.tdoc, this.pdoc] = await Promise.all([ + contest.get(domainId, tid, document.TYPE_HOMEWORK), + problem.get(domainId, pid, this.user._id), + ]); + } + + async get({ domainId, tid, pid }) { + const [tsdoc, udoc] = await Promise.all( + contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK), + user.getById(domainId, this.tdoc.owner), + ); + const attended = tsdoc && tsdoc.attend === 1; + if (!contest.isDone(this.tdoc)) { + if (!attended) throw new HomeworkNotAttendedError(tid); + if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid); + } + if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid, tid); + const path = [ + ['homework_main', '/homework'], + [this.tdoc.title, `/homework/${tid}`, true], + [this.pdoc.title, null, true], + ]; + this.response.template = 'problem_detail.html'; + this.response.body = { + tdoc: this.tdoc, pdoc: this.pdoc, tsdoc, udoc, attended, path, + }; + } +} + +class HomeworkDetailProblemSubmitHandler extends HomeworkHandler { + async get({ domainId, tid, pid }) { + const uid = this.user._id; + const [tdoc, pdoc] = await Promise.all([ + contest.get(domainId, tid, document.TYPE_HOMEWORK), + problem.get(domainId, pid, uid), + ]); + const [tsdoc, udoc] = await Promise.all([ + contest.getStatus(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK), + user.get_by_uid(tdoc.owner), + ]); + const attended = tsdoc && tsdoc.attend === 1; + if (!attended) { throw new HomeworkNotAttendedError(tdoc.docId); } + if (!contest.isOngoing(tdoc)) { throw new HomeworkNotLiveError(tdoc.docId); } + if (!tdoc.pids.includes(pid)) { throw new ProblemNotFoundError(domainId, pid, tdoc.docId); } + const rdocs = this.canShowRecord(tdoc) + ? await record.getUserInProblemMulti(domainId, uid, pdoc.docId, true) + .sort('_id', -1) + .limit(10) + .toArray() + : []; + const path = [ + ['homework_main', '/homework'], + [tdoc.title, `/homework/${tid}`, true], + [pdoc.title, `/homework/${tid}/p/${pid}`, true], + ['homework_detail_problem_submit', null], + ]; + this.response.template = 'problem_submit.html'; + this.response.body = { + tdoc, pdoc, rdocs, tsdoc, udoc, attended, path, + }; + } + + async post({ + domainId, tid, pid, code, lang, + }) { + this.limitRate('add_record', 60, 100); + const tsdoc = await contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK); + if (!tsdoc.attend) throw new HomeworkNotAttendedError(tid); + if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid); + if (!this.tdoc.pids.includes(pid)) throw new ProblemNotFoundError(domainId, pid); + const rid = await record.add(domainId, { + pid, lang, code, uid: this.user._id, tid, hidden: true, ttype: document.TYPE_HOMEWORK, + }); + await contest.updateStatus(domainId, tid, this.user._id, + rid, pid, false, 0, document.TYPE_HOMEWORK); + this.response.body = { tid, rid }; + if (this.canShowRecord(this.tdoc)) this.response.redirect = `/record/${rid}`; + } +} + +class HomeworkCreateHandler extends HomeworkHandler { + async get() { + const beginAt = new Date().delta({ day: 1 }); + const penaltySince = beginAt.delta({ day: 7 }); + const path = [ + ['homework_create', null], + ]; + this.response.template = 'homework_edit.html'; + this.response.body = { + path, + dateBeginText: beginAt.format('%Y-%m-%d'), + timeBeginText: '00:00', + datePenaltyText: penaltySince.format('%Y-%m-%d'), + timePenaltyText: '23:59', + pids: '1000, 1001', + extensionDays: 1, + page_name: 'homework_create', + }; + } + + async post({ + domainId, title, content, beginAtDate, beginAtTime, + penaltySinceDate, penaltySinceTime, extensionDays, + penaltyRules, pids, + }) { + let beginAt; + let penaltySince; + try { + beginAt = new Date(Date.parse(`${beginAtDate} ${beginAtTime.replace('-', ':')}`)); + } catch (e) { + throw new ValidationError('beginAtDate', 'beginAtTime'); + } + try { + penaltySince = new Date(Date.parse(`${penaltySinceDate} ${penaltySinceTime.replace('-', ':')}`)); + } catch (e) { + throw new ValidationError('end_at_date', 'end_at_time'); + } + const endAt = penaltySince.delta({ days: extensionDays }); + if (beginAt >= penaltySince) { throw new ValidationError('end_at_date', 'end_at_time'); } + if (penaltySince > endAt) throw new ValidationError('extension_days'); + await this.verifyProblems(domainId, pids); + const tid = await contest.add(domainId, title, content, this.user._id, + 'homework', beginAt, endAt, pids, { penaltySince, penaltyRules }, document.TYPE_HOMEWORK); + this.response.body = { tid }; + this.response.redirect = `/homework/${tid}`; + } +} + +class HomeworkEditHandler extends HomeworkHandler { + async get({ domainId, tid }) { + const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); + if (tdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_HOMEWORK); + const extensionDays = Math.round( + (tdoc.endAt.getTime() - tdoc.penaltySince.getTime()) / 36000 / 24, + ) / 100; + const path = [ + ['homework_main', '/homework'], + [tdoc.title, `/homework/${tid}`, true], + ['homework_edit', null], + ]; + this.response.template = 'homework_edit.html'; + this.response.body = { + tdoc, + dateBeginText: tdoc.beginAt.format('%Y-%m-%d'), + timeBeginText: tdoc.beginAt.format('%H:%M'), + datePenaltyText: tdoc.penaltySince.format('%Y-%m-%d'), + timePenaltyText: tdoc.penaltySince.format('%H:%M'), + extensionDays, + penaltyRules: _formatPenaltyRules(tdoc.penaltyRules), + pids: tdoc.pids.join(','), + path, + }; + } + + async post({ + domainId, title, content, beginAtDate, beginAtTime, + penaltySinceDate, penaltySinceTime, extensionDays, + penaltyRules, pids, tid, + }) { + const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK); + if (tdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_HOMEWORK); + let beginAt; + let penaltySince; + try { + beginAt = new Date(Date.parse(`${beginAtDate} ${beginAtTime.replace('-', ':')}`)); + } catch (e) { + throw new ValidationError('beginAtDate', 'beginAtTime'); + } + try { + penaltySince = new Date(Date.parse(`${penaltySinceDate} ${penaltySinceTime.replace('-', ':')}`)); + } catch (e) { + throw new ValidationError('endAtDate', 'endAtTime'); + } + const endAt = penaltySince.delta({ days: extensionDays }); + if (beginAt >= penaltySince) throw new ValidationError('end_at_date', 'end_at_time'); + await this.verifyProblems(domainId, pids); + await contest.edit(domainId, tid, { + title, content, beginAt, endAt, pids, penaltySince, penaltyRules, + }, document.TYPE_HOMEWORK); + if (tdoc.beginAt !== beginAt + || tdoc.endAt !== endAt + || tdoc.penaltySince !== penaltySince + || new Set(tdoc.pids) !== new Set(pids)) { + await contest.recalcStatus(domainId, document.TYPE_HOMEWORK, tdoc.docId); + } + this.response.body = { tid }; + this.response.redirect = `/homework/${tid}`; + } +} + +async function apply() { + contest.isHomeWorkExtended = (tdoc) => { + const now = new Date().getTime(); + return tdoc.penaltySince.getTime() <= now && now < tdoc.endAt.getTime(); + }; + contest.RULES.homework = { + TEXT: 'Assignment', + check: () => { }, + stat: (tdoc, journal) => { + const effective = {}; + for (const j of journal) { + if (tdoc.pids.includes(j.pid) + && !effective.includes(j.pid) + && effective[j.pid].accept) { + effective[j.pid] = j; + } + } + function time(jdoc) { + const real = jdoc.rid.generationTime - tdoc.beginAt.getTime() / 1000; + return Math.floor(real); + } + function penaltyScore(jdoc) { + const { score } = jdoc; + const exceedSeconds = Math.floor( + jdoc.rid.generationTime - tdoc.penaltySince.getTime() / 1000, + ); + if (exceedSeconds < 0) return score; + let coefficient = 1; + const keys = Object.keys(tdoc.penaltyRules) + .map((k) => parseFloat(k)).sort((a, b) => a - b); + for (const i of keys) { + if (i * 3600 <= exceedSeconds) coefficient = tdoc.penaltyRules[i]; + else break; + } + return score * coefficient; + } + const detail = []; + for (const j of effective) { + detail.push({ ...j, penaltyScore: penaltyScore(j), time: time(j) }); + } + return { + score: Math.sum(detail.map((d) => d.score)), + penaltyScore: Math.sum(detail.map((d) => d.penaltySince)), + time: Math.sum(detail.map((d) => d.time)), + 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), + }; + Route('/homework', HomeworkMainHandler, PERM_VIEW_HOMEWORK); + Route('/homework/create', HomeworkCreateHandler, PERM_CREATE_HOMEWORK); + Route('/homework/:tid', HomeworkDetailHandler, PERM_VIEW_HOMEWORK); + Route('/homework/:tid/p/:pid', HomeworkDetailProblemHandler, PERM_VIEW_HOMEWORK); + Route('/homework/:tid/p/:pid/submit', HomeworkDetailProblemSubmitHandler, PERM_SUBMIT_PROBLEM); + Route('/homework/:tid/edit', HomeworkEditHandler); +} + +global.Hydro.handler.homework = { + HomeworkMainHandler, + HomeworkCreateHandler, + HomeworkDetailHandler, + HomeworkDetailProblemHandler, + HomeworkDetailProblemSubmitHandler, + HomeworkEditHandler, + apply, +}; + +/* + +@app.route('/homework/{tid:\w{24}}/code', 'homework_code') +class HomeworkCodeHandler(base.OperationHandler): + @base.limit_rate('homework_code', 3600, 60) + @base.route_argument + @base.require_perm(builtin.PERM_VIEW_HOMEWORK) + @base.require_perm(builtin.PERM_READ_RECORD_CODE) + @base.sanitize + async def get(this, *, tid: objectid.ObjectId): + tdoc, tsdocs = await contest.get_and_list_status(domainId, document.TYPE_HOMEWORK, tid) + rnames = {} + for tsdoc in tsdocs: + for pdetail in tsdoc.get('detail', []): + rnames[pdetail['rid']] = 'U{}_P{}_R{}'.format(tsdoc['uid'], pdetail['pid'], pdetail['rid']) + output_buffer = io.BytesIO() + zip_file = zipfile.ZipFile(output_buffer, 'a', zipfile.ZIP_DEFLATED) + rdocs = record.get_multi(get_hidden=True, _id={'$in': list(rnames.keys())}) + async for rdoc in rdocs: + zip_file.writestr(rnames[rdoc['_id']] + '.' + rdoc['lang'], rdoc['code']) + # mark all files as created in Windows :p + for zfile in zip_file.filelist: + zfile.create_system = 0 + zip_file.close() + + await this.binary(output_buffer.getvalue(), 'application/zip', + file_name='{}.zip'.format(tdoc['title'])) + + +@app.route('/homework/{tid}/scoreboard', 'homework_scoreboard') +class HomeworkScoreboardHandler(contest.ContestMixin, base.Handler): + @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): + tdoc, rows, udict = await this.get_scoreboard(document.TYPE_HOMEWORK, tid) + page_title = this.translate('homework_scoreboard') + path_components = this.build_path( + (this.translate('homework_main'), this.reverse_url('homework_main')), + (tdoc['title'], this.reverse_url('homework_detail', tid=tdoc['docId'])), + (page_title, None)) + dudict = await domain.get_dict_user_by_uid(domain_id=domainId, uids=udict.keys()) + this.render('contest_scoreboard.html', tdoc=tdoc, rows=rows, dudict=dudict, + page_title=page_title, path_components=path_components) + + +@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/status.js b/hydro/handler/misc.js similarity index 65% rename from hydro/handler/status.js rename to hydro/handler/misc.js index a2f64a9bb..7caf7c5ea 100644 --- a/hydro/handler/status.js +++ b/hydro/handler/misc.js @@ -1,9 +1,19 @@ + const { PERM_JUDGE } = require('../permission'); -const { Route, Handler } = require('../service/server'); +const file = require('../model/file'); const db = require('../service/db'); +const { Route, Handler } = require('../service/server'); const coll = db.collection('status'); +class FileDownloadHandler extends Handler { + async get({ id, secret, name }) { + if (name) name = Buffer.from(name, 'base64').toString(); + this.response.attachment(name || id); + this.response.body = await file.get(id, secret); + } +} + class StatusHandler extends Handler { async get() { const stats = await coll.find().sort({ type: 1, updateAt: -1 }).toArray(); @@ -27,8 +37,15 @@ class StatusUpdateHandler extends Handler { } async function apply() { + Route('/fs/:id/:secret', FileDownloadHandler); + Route('/fs/:id/:name/:secret', FileDownloadHandler); Route('/status', StatusHandler); Route('/status/update', StatusUpdateHandler); } -global.Hydro.handler.status = module.exports = { StatusHandler, StatusUpdateHandler, apply }; +global.Hydro.handler.misc = module.exports = { + FileDownloadHandler, + StatusHandler, + StatusUpdateHandler, + apply, +}; diff --git a/hydro/handler/user.js b/hydro/handler/user.js index d21e46245..287d860f6 100644 --- a/hydro/handler/user.js +++ b/hydro/handler/user.js @@ -4,10 +4,11 @@ const token = require('../model/token'); const system = require('../model/system'); const { sendMail } = require('../lib/mail'); const misc = require('../lib/misc'); -const { PERM_REGISTER_USER, PERM_LOGGEDIN } = require('../permission'); +const { PERM_LOGGEDIN } = require('../permission'); const { UserAlreadyExistError, InvalidTokenError, VerifyPasswordError, UserNotFoundError, LoginError, SystemError, + PermissionError, } = require('../error'); class UserLoginHandler extends Handler { @@ -32,10 +33,6 @@ class UserLoginHandler extends Handler { } class UserLogoutHandler extends Handler { - async prepare() { - this.checkPerm(PERM_LOGGEDIN); - } - async get() { this.response.template = 'user_logout.html'; } @@ -46,8 +43,10 @@ class UserLogoutHandler extends Handler { } class UserRegisterHandler extends Handler { - async prepare() { - this.checkPerm(PERM_REGISTER_USER); + async prepare() { // eslint-disable-line class-methods-use-this + if (!await system.get('user.register')) { + throw new PermissionError('Register'); + } } async get() { @@ -73,10 +72,6 @@ class UserRegisterHandler extends Handler { } class UserRegisterWithCodeHandler extends Handler { - async prepare() { - this.checkPerm(PERM_REGISTER_USER); - } - async get({ code }) { this.response.template = 'user_register_with_code.html'; const { mail } = await token.get(code, token.TYPE_REGISTRATION); @@ -174,7 +169,7 @@ async function apply() { Route('/login', module.exports.UserLoginHandler); Route('/register', module.exports.UserRegisterHandler); Route('/register/:code', module.exports.UserRegisterWithCodeHandler); - Route('/logout', module.exports.UserLogoutHandler); + Route('/logout', module.exports.UserLogoutHandler, PERM_LOGGEDIN); Route('/lostpass', module.exports.UserLostPassHandler); Route('/lostpass/:code', module.exports.UserLostPassWithCodeHandler); Route('/user/search', module.exports.UserSearchHandler); diff --git a/hydro/lib/nav.js b/hydro/lib/nav.js index d0009e0e7..e36010850 100644 --- a/hydro/lib/nav.js +++ b/hydro/lib/nav.js @@ -11,6 +11,7 @@ const Item = (path, name, prefix, perm) => { Item('/', 'domain_main', 'domain_main'); Item('/p', 'problem_main', 'problem', permission.PERM_VIEW_PROBLEM); Item('/t', 'training_main', 'training', permission.PERM_VIEW_TRAINING); +Item('/homework', 'homework_main', 'homework', permission.PERM_VIEW_HOMEWORK); Item('/discuss', 'discussion_main', 'discussion', permission.PERM_VIEW_DISCUSSION); Item('/c', 'contest_main', 'contest', permission.PERM_VIEW_CONTEST); Item('/manage', 'manage', 'manage', permission.PERM_MANAGE); diff --git a/hydro/lib/template.js b/hydro/lib/template.js index 4a93c77fb..023f5b818 100644 --- a/hydro/lib/template.js +++ b/hydro/lib/template.js @@ -29,21 +29,22 @@ class Nunjucks extends nunjucks.Environment { const env = new Nunjucks(); function render(name, state) { - Object.assign(state, { - typeof: (o) => typeof o, - static_url: (str) => `/${str}`, - datetimeSpan: misc.datetimeSpan, - paginate: misc.paginate, - perm, - status: builtin.status, - size: misc.size, - gravatar: misc.gravatar, - model, - Context: global.Hydro.ui, - isIE: (str) => str.includes('MSIE') || str.includes('rv:11.0'), - }); return new Promise((resolve, reject) => { - env.render(name, state, (err, res) => { + env.render(name, { + page_name: name.split('.')[0], + ...state, + typeof: (o) => typeof o, + static_url: (str) => `/${str}`, + datetimeSpan: misc.datetimeSpan, + paginate: misc.paginate, + perm, + status: builtin.status, + size: misc.size, + gravatar: misc.gravatar, + model, + Context: global.Hydro.ui, + isIE: (str) => str.includes('MSIE') || str.includes('rv:11.0'), + }, (err, res) => { if (err) reject(err); else resolve(res); }); diff --git a/hydro/loader.js b/hydro/loader.js index 6f787aa1b..9656c3804 100644 --- a/hydro/loader.js +++ b/hydro/loader.js @@ -257,7 +257,7 @@ async function load() { const builtinHandler = [ 'home', 'problem', 'record', 'judge', 'user', 'contest', 'training', 'discussion', 'manage', 'import', - 'fs', 'status', + 'misc', 'homework', ]; for (const i of builtinHandler) require(`./handler/${i}`); await model(); diff --git a/hydro/model/contest.js b/hydro/model/contest.js index 5b4cb4876..9c57fcded 100644 --- a/hydro/model/contest.js +++ b/hydro/model/contest.js @@ -207,7 +207,7 @@ const RULES = { * @returns {ObjectID} tid */ function add(domainId, title, content, owner, rule, - beginAt = new Date(), endAt = new Date(), pids = [], data = {}) { + beginAt = new Date(), endAt = new Date(), pids = [], data = {}, type = document.TYPE_CONTEST) { validator.checkTitle(title); validator.checkContent(content); if (!this.RULES[rule]) throw new ValidationError('rule'); @@ -216,7 +216,7 @@ function add(domainId, title, content, owner, rule, content, owner, title, rule, beginAt, endAt, pids, attend: 0, }); this.RULES[rule].check(data); - return document.add(domainId, content, owner, document.TYPE_CONTEST, null, null, null, { + return document.add(domainId, content, owner, type, null, null, null, { ...data, title, rule, beginAt, endAt, pids, attend: 0, }); } @@ -225,9 +225,10 @@ function add(domainId, title, content, owner, rule, * @param {string} domainId * @param {ObjectID} tid * @param {object} $set + * @param {number} type * @returns {Tdoc} tdoc after modification */ -async function edit(domainId, tid, $set) { +async function edit(domainId, tid, $set, type = document.TYPE_CONTEST) { if ($set.title) validator.checkTitle($set.title); if ($set.content) validator.checkIntro($set.content); if ($set.rule) { if (!this.RULES[$set.rule]) throw new ValidationError('rule'); } @@ -236,33 +237,37 @@ async function edit(domainId, tid, $set) { throw new ValidationError('beginAt', 'endAt'); } } - const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); + const tdoc = await document.get(domainId, type, tid); if (!tdoc) throw new ContestNotFoundError(tid); this.RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set)); - return await document.set(domainId, document.TYPE_CONTEST, tid, $set); + return await document.set(domainId, type, tid, $set); } /** * @param {string} domainId * @param {ObjectID} tid + * @param {number} type * @returns {Tdoc} */ -async function get(domainId, tid) { - const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid); +async function get(domainId, tid, type = document.TYPE_CONTEST) { + const tdoc = await document.get(domainId, type, tid); if (!tdoc) throw new ContestNotFoundError(tid); return tdoc; } -async function updateStatus(domainId, tid, uid, rid, pid, accept = false, score = 0) { +async function updateStatus( + domainId, tid, uid, rid, pid, + accept = false, score = 0, type = document.TYPE_CONTEST, +) { await get(domainId, tid); - const tsdoc = await document.revPushStatus(domainId, document.TYPE_CONTEST, tid, uid, 'journal', { + const tsdoc = await document.revPushStatus(domainId, type, tid, uid, 'journal', { rid, pid, accept, score, }); if (!tsdoc.attend) throw new ContestNotAttendedError(tid, uid); } -function getStatus(domainId, tid, uid) { - return document.getStatus(domainId, document.TYPE_CONTEST, tid, uid); +function getStatus(domainId, tid, uid, type = document.TYPE_CONTEST) { + return document.getStatus(domainId, type, tid, uid); } async function getListStatus(domainId, uid, tids) { @@ -272,13 +277,13 @@ async function getListStatus(domainId, uid, tids) { return r; } -async function attend(domainId, tid, uid) { +async function attend(domainId, tid, uid, type = document.TYPE_CONTEST) { try { - await document.cappedIncStatus(domainId, document.TYPE_CONTEST, tid, uid, 'attend', 1, 0, 1); + await document.cappedIncStatus(domainId, type, tid, uid, 'attend', 1, 0, 1); } catch (e) { throw new ContestAlreadyAttendedError(tid, uid); } - await document.inc(domainId, document.TYPE_CONTEST, tid, 'attend', 1); + await document.inc(domainId, type, tid, 'attend', 1); return {}; } @@ -355,6 +360,38 @@ function setStatus(domainId, tid, uid, $set) { return document.setStatus(domainId, document.TYPE_CONTEST, tid, uid, $set); } +function count(domainId, query, type = document.TYPE_CONTEST) { + return document.count(domainId, type, query); +} + +function getMulti(domainId, query = {}, type = document.TYPE_CONTEST) { + return document.getMulti(domainId, type, query); +} + +function _getStatusJournal(tsdoc) { + return tsdoc.journal.sort((a, b) => (a.rid.generationTime - b.rid.generationTime)); +} + +async function recalcStatus(domainId, tid, type) { + const [tdoc, tsdocs] = await Promise.all([ + document.get(domainId, type, tid), + document.getMultiStatus(domainId, type, { docId: tid }), + ]); + const tasks = []; + for (const tsdoc of tsdocs) { + if (tsdoc.journal) { + const journal = _getStatusJournal(tsdoc); + const stats = RULES[tdoc.rule].stat(tdoc, journal); + tasks.push( + document.revSetStatus( + domainId, type, tid, + tsdoc.uid, tsdoc.rev, { journal, ...stats }, false, + ), + ); + } + } + return await Promise.all(tasks); +} global.Hydro.model.contest = module.exports = { RULES, ContestHandlerMixin, @@ -365,9 +402,10 @@ global.Hydro.model.contest = module.exports = { get, updateStatus, getStatus, - count: (domainId, query) => document.count(domainId, document.TYPE_CONTEST, query), - getMulti: (domainId, query = {}) => document.getMulti(domainId, document.TYPE_CONTEST, query), + count, + getMulti, setStatus, + recalcStatus, isNew, isUpcoming, isNotStarted, @@ -388,27 +426,3 @@ global.Hydro.model.contest = module.exports = { ? 'ongoing' : 'finished'), }; - -/* - -def _get_status_journal(tsdoc): - # Sort and uniquify journal of the contest status document, by rid. - return [list(g)[-1] for _, g in itertools.groupby(sorted(tsdoc['journal'], key=journal_key_func), - key=journal_key_func)] - -@argmethod.wrap -async def recalc_status(domainId: str, doc_type: int, cid: objeccid.Objeccid): - if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: - raise error.InvalidArgumentError('doc_type') - tdoc = await document.get(domainId, doc_type, cid) - async with document.get_multi_status(domainId=domainId, - doc_type=doc_type, - doc_id=tdoc.docId) as tsdocs: - async for tsdoc in tsdocs: - if 'journal' not in tsdoc or not tsdoc['journal']: - continue - journal = _get_status_journal(tsdoc) - stats = RULES[tdoc['rule']].stat_func(tdoc, journal) - await document.rev_set_status(domainId, doc_type, cid, tsdoc['uid'], tsdoc['rev'], - return_doc=False, journal=journal, **stats) -*/ diff --git a/hydro/model/record.js b/hydro/model/record.js index 105405e5e..bebda80ff 100644 --- a/hydro/model/record.js +++ b/hydro/model/record.js @@ -3,7 +3,7 @@ const { ObjectID } = require('bson'); const { STATUS_WAITING } = require('./builtin').STATUS; const task = require('./task'); const problem = require('./problem'); -const { RecordNotFoundError } = require('../error'); +const { RecordNotFoundError, PermissionError } = require('../error'); const db = require('../service/db'); const coll = db.collection('record'); @@ -35,10 +35,11 @@ async function add(domainId, data) { * @param {ObjectID} rid * @returns {import('../interface').Record} */ -async function get(domainId, rid) { +async function get(domainId, rid, showHidden = false) { const _id = new ObjectID(rid); const rdoc = await coll.findOne({ domainId, _id }); if (!rdoc) throw new RecordNotFoundError(rid); + if (rdoc.hidden && !showHidden) throw new PermissionError(rid); return rdoc; } @@ -77,15 +78,18 @@ function count(domainId, query) { return coll.find({ domainId, ...query }).count(); } -async function getList(domainId, rids) { +async function getList(domainId, rids, showHidden = false) { const r = {}; // eslint-disable-next-line no-await-in-loop - for (const rid of rids) r[rid] = await get(domainId, rid); + for (const rid of rids) r[rid] = await get(domainId, rid, showHidden); return r; } -function getUserInProblemMulti(domainId, uid, pid) { - return coll.find({ domainId, owner: uid, pid }); +function getUserInProblemMulti(domainId, uid, pid, getHidden = false) { + if (getHidden) return coll.find({ domainId, owner: uid, pid }); + return coll.find({ + domainId, owner: uid, pid, hidden: false, + }); } async function judge(domainId, rid) { diff --git a/hydro/model/user.js b/hydro/model/user.js index 36f3dd6ae..8b36d96d4 100644 --- a/hydro/model/user.js +++ b/hydro/model/user.js @@ -60,6 +60,7 @@ async function getById(domainId, _id) { } async function getList(domainId, uids) { + uids = new Set(uids); const r = {}; // eslint-disable-next-line no-await-in-loop for (const uid of uids) r[uid] = await getById(domainId, uid); diff --git a/hydro/permission.js b/hydro/permission.js index e7fc93531..9f80e3e70 100644 --- a/hydro/permission.js +++ b/hydro/permission.js @@ -1,7 +1,6 @@ const perm = { PERM_MANAGE: '#', PERM_LOGGEDIN: '0', - PERM_REGISTER_USER: '1', PERM_MOD_BADGE: '3', PERM_CREATE_PROBLEM: '5', PERM_EDIT_PROBLEM: '6', @@ -37,6 +36,10 @@ const perm = { PERM_VIEW_TRAINING: 'a', PERM_CREATE_TRAINING: 'b', PERM_EDIT_TRAINING: 'c', + PERM_VIEW_HOMEWORK: 'e', + PERM_CREATE_HOMEWORK: 'f', + PERM_EDIT_HOMEWORK: 'g', + PERM_ATTEND_HOMEWORK: 'h', PERM_JUDGE: 'd', }; @@ -47,7 +50,6 @@ perm.PERM_ALL = PERM_ALL; const Permission = (family, key, desc) => ({ family, key, desc }); const PERMS = [ - Permission('perm_general', perm.PERM_REGISTER_USER, 'Register new account'), Permission('perm_general', perm.PERM_MANAGE, 'Manage the system'), Permission('perm_general', perm.PERM_MOD_BADGE, 'Show MOD badge'), Permission('perm_problem', perm.PERM_CREATE_PROBLEM, 'Create problems'), @@ -99,7 +101,7 @@ perm.PERM_BASIC = perm.PERM_VIEW_PROBLEM + perm.PERM_VIEW_TRAINING + perm.PERM_VIEW_CONTEST + perm.PERM_VIEW_CONTEST_SCOREBOARD - + perm.PERM_REGISTER_USER; + + perm.PERM_VIEW_HOMEWORK; perm.PERM_DEFAULT = perm.PERM_BASIC + perm.PERM_SUBMIT_PROBLEM @@ -110,7 +112,8 @@ perm.PERM_DEFAULT = perm.PERM_BASIC + perm.PERM_REPLY_DISCUSSION + perm.PERM_ATTEND_CONTEST + perm.PERM_CREATE_TRAINING - + perm.PERM_LOGGEDIN; + + perm.PERM_LOGGEDIN + + perm.PERM_ATTEND_HOMEWORK; perm.PERM_ADMIN = perm.PERM_ALL; diff --git a/hydro/service/server.js b/hydro/service/server.js index cde098af0..7589c4356 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -1,7 +1,9 @@ +const assert = require('assert'); const path = require('path'); const os = require('os'); const { ObjectID } = require('bson'); const Koa = require('koa'); +const yaml = require('js-yaml'); const morgan = require('koa-morgan'); const Body = require('koa-body'); const Router = require('koa-router'); @@ -24,6 +26,55 @@ const app = new Koa(); let server; const router = new Router(); +const _validateObjectId = (id, key) => { + if (ObjectID.isValid(id)) return new ObjectID(id); + throw new ValidationError(key); +}; + +const validate = { + tid: _validateObjectId, + rid: _validateObjectId, + did: _validateObjectId, + drid: _validateObjectId, + drrid: _validateObjectId, + psid: _validateObjectId, + psrid: _validateObjectId, + docId: _validateObjectId, + mongoId: _validateObjectId, + pid: (pid) => (Number.isSafeInteger(parseInt(pid)) ? parseInt(pid) : pid), + content: validator.checkContent, + title: validator.checkTitle, + uid: parseInt(validator.checkUid), + password: validator.checkPassword, + mail: validator.checkEmail, + uname: validator.checkUname, + page: (page) => { + if (Number.isSafeInteger(parseInt(page))) page = parseInt(page); + if (page <= 0) throw new ValidationError('page'); + return page; + }, + duration: (duration) => { + if (!Number.isNaN(parseFloat(duration))) duration = parseFloat(duration); + if (duration <= 0) throw new ValidationError('duration'); + return duration; + }, + pids: (pids) => pids.split(',').map((i) => i.trim()), + role: validator.checkRole, + roles: (roles) => { + for (const i of roles) validator.checkRole(i); + return roles; + }, + penaltyRules: (penaltyRules) => { + try { + penaltyRules = yaml.safeLoad(penaltyRules); + } catch (e) { + throw new ValidationError('penalty_rules', 'parse error'); + } + assert(typeof penaltyRules === 'object', new ValidationError('penalty_rules', 'invalid format')); + return penaltyRules; + }, +}; + async function prepare() { server = http.createServer(app.callback()); app.keys = await system.get('session.keys'); @@ -240,9 +291,7 @@ class Handler { } } -const check = ['tid', 'rid', 'did', 'drid', 'drrid', 'psid', 'psrid', 'docId', 'mongoId']; - -function Route(route, RouteHandler) { +function Route(route, RouteHandler, permission = null) { router.all(route, async (ctx) => { const h = new RouteHandler(ctx); try { @@ -253,29 +302,14 @@ function Route(route, RouteHandler) { }; if (h.___prepare) await h.___prepare(args); + if (permission) h.checkPerm(permission); + try { - for (const l of check) { - if (args[l]) { - args[l] = new ObjectID(args[l]); - if (!args[l]) throw new ValidationError(l); + for (const key in validate) { + if (args[key]) { + args[key] = validate[key](args[key], key); } } - if (args.pid) { - if (Number.isInteger(Number(args.pid))) args.pid = Number(args.pid); - } - 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.password) validator.checkPassword(args.password); - if (args.mail) validator.checkEmail(args.mail); - if (args.uname) validator.checkUname(args.uname); - if (args.page) args.page = parseInt(args.page); - if (args.duration) args.duration = parseFloat(args.duration); - if (args.pids) args.pids = args.pids.split(',').map((i) => i.trim()); - if (args.role) validator.checkRole(args.role); - if (args.roles) { - for (const i of args.roles) validator.checkRole(i); - } } catch (e) { if (e instanceof ValidationError) throw e; throw new ValidationError('Argument check failed'); @@ -392,6 +426,11 @@ function Middleware(middleware) { app.use(middleware); } +function Validate(key, func) { + if (validate[key]) validate[key].push(func); + else validate[key] = [func]; +} + async function start() { app.use(morgan(':method :url :status :res[content-length] - :response-time ms')); app.use(router.routes()).use(router.allowedMethods()); @@ -401,5 +440,5 @@ async function start() { } global.Hydro.service.server = module.exports = { - Handler, ConnectionHandler, Route, Connection, Middleware, prepare, start, + Handler, ConnectionHandler, Route, Connection, Middleware, Validate, prepare, start, }; diff --git a/hydro/utils.js b/hydro/utils.js index 35326136c..c526df49d 100644 --- a/hydro/utils.js +++ b/hydro/utils.js @@ -14,6 +14,10 @@ Array.isDiff = function isDiff(a, b) { return false; }; +/** + * @param {string} format %Y-%m-%d %H:%M:%S + * @returns {string} Formatted date string + */ Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') { return fmt .replace('%Y', this.getFullYear()) @@ -25,6 +29,19 @@ Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') { .replace('%S', this.getSeconds()); }; +/** + * @param {object} param0 + * @returns {Date} + */ +Date.prototype.delta = function delta({ + year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0, +}) { + let ts = this.getTime(); + ts += second * 1000 + minute * 60000 + hour * 60 * 60000; + ts += day * 24 * 60 * 60000 + month * 30 * 24 * 60 * 60000 + year * 365 * 24 * 60 * 60000; + return new Date(ts); +}; + Set.isSuperset = function isSuperset(set, subset) { for (const elem of subset) { if (!set.has(elem)) return false; diff --git a/templates/components/homework.html b/templates/components/homework.html new file mode 100644 index 000000000..b6a69b27b --- /dev/null +++ b/templates/components/homework.html @@ -0,0 +1,8 @@ +{% import "components/user.html" as user with context %} +{% macro render_time(tdoc_time) %} +{{ datetimeSpan(tdoc_time, false, '%Y-%m-%d %H:%M')|safe }} +{% endmacro %} + +{% macro render_extension(tdoc) %} +{{ ((tdoc['endAt'].getTime() - tdoc['penaltySince'].getTime())/ 1000 / 3600/ 24)|round(1) }} +{% endmacro %} diff --git a/templates/contest_detail.html b/templates/contest_detail.html index 6115e6329..54f651dc7 100644 --- a/templates/contest_detail.html +++ b/templates/contest_detail.html @@ -1,4 +1,3 @@ -{% set page_name = "contest_detail" %} {% extends "layout/basic.html" %} {% import "components/contest.html" as contest with context %} {% import "components/record.html" as record with context %} diff --git a/templates/contest_main.html b/templates/contest_main.html index 849852d5f..0b8b11747 100644 --- a/templates/contest_main.html +++ b/templates/contest_main.html @@ -1,4 +1,3 @@ -{% set page_name = "contest_main" %} {% extends "layout/basic.html" %} {% import "components/contest.html" as contest with context %} {% macro render_important_contest(view, title, tdoc) %} diff --git a/templates/contest_scoreboard.html b/templates/contest_scoreboard.html index 5b8d72b07..59e2f1434 100644 --- a/templates/contest_scoreboard.html +++ b/templates/contest_scoreboard.html @@ -1,4 +1,3 @@ -{% set page_name = "contest_scoreboard" %} {% extends "layout/basic.html" %} {% block content %}
diff --git a/templates/discussion_create.html b/templates/discussion_create.html index ec114d619..6362dd208 100644 --- a/templates/discussion_create.html +++ b/templates/discussion_create.html @@ -1,4 +1,3 @@ -{% set page_name = "discussion_create" %} {% extends "layout/basic.html" %} {% block content %}
diff --git a/templates/discussion_detail.html b/templates/discussion_detail.html index df9b602aa..12b7281c1 100644 --- a/templates/discussion_detail.html +++ b/templates/discussion_detail.html @@ -1,4 +1,3 @@ -{% set page_name = "discussion_detail" %} {% extends "layout/basic.html" %} {% import "components/comments_discussion.html" as comments with context %} {% block content %} diff --git a/templates/error.html b/templates/error.html index 544d5b9ab..fd482c500 100644 --- a/templates/error.html +++ b/templates/error.html @@ -1,5 +1,4 @@ {% set no_path_section = true %} -{% set page_name = "error" %} {% extends "layout/basic.html" %} {% block content %}
diff --git a/templates/home_messages.html b/templates/home_messages.html index 902753e33..6c96dd2ea 100644 --- a/templates/home_messages.html +++ b/templates/home_messages.html @@ -1,4 +1,3 @@ -{% set page_name = "home_messages" %} {% extends "layout/home_base.html" %} {% block home_content %}