diff --git a/hydro/handler/contest.js b/hydro/handler/contest.js index 8d7f4a8bb..14a13d500 100644 --- a/hydro/handler/contest.js +++ b/hydro/handler/contest.js @@ -5,6 +5,7 @@ const { const { PERM_CREATE_CONTEST, PERM_EDIT_CONTEST } = require('../permission'); const paginate = require('../lib/paginate'); const contest = require('../model/contest'); +const document = require('../model/document'); const problem = require('../model/problem'); const record = require('../model/record'); const user = require('../model/user'); @@ -241,14 +242,11 @@ class ContestProblemSubmitHandler extends ContestProblemHandler { pid: this.pdoc.docId, uid: this.user._id, tid: this.tdoc.docId, + ttype: document.TYPE_CONTEST, lang, code, - hidden: true, }); - await Promise.all([ - record.judge(domainId, rid), - contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId), - ]); + await contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId); if (!this.canShowRecord(this.tdoc)) { this.response.body = { tid }; this.response.redirect = `/c/${tid}`; diff --git a/hydro/handler/homework.js b/hydro/handler/homework.js index 4a5560cf5..19307d534 100644 --- a/hydro/handler/homework.js +++ b/hydro/handler/homework.js @@ -1,3 +1,4 @@ +const yaml = require('js-yaml'); const { ValidationError, HomeworkNotLiveError, ProblemNotFoundError, HomeworkNotAttendedError, @@ -5,6 +6,7 @@ const { const { PERM_VIEW_HOMEWORK, PERM_ATTEND_HOMEWORK, PERM_VIEW_PROBLEM, PERM_SUBMIT_PROBLEM, PERM_CREATE_HOMEWORK, PERM_EDIT_HOMEWORK, + PERM_VIEW_HOMEWORK_SCOREBOARD, } = require('../permission'); const { Route, Handler } = require('../service/server'); const system = require('../model/system'); @@ -15,16 +17,8 @@ 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 misc = require('../lib/misc'); +const ranked = require('../lib/rank'); const HomeworkHandler = contest.ContestHandlerMixin(Handler); @@ -35,7 +29,7 @@ class HomeworkMainHandler extends HomeworkHandler { for (const tdoc of tdocs) { console.log(tdoc); const cal = { ...tdoc }; - if (contest.isHomeWorkExtended(tdoc) || contest.isDone(tdoc)) { + if (contest.isHomeworkExtended(tdoc) || contest.isDone(tdoc)) { cal.endAt = tdoc.endAt; cal.penaltySince = tdoc.penaltySince; } else { @@ -60,14 +54,14 @@ class HomeworkDetailHandler extends HomeworkHandler { let attended = false; if (tsdoc) { attended = tsdoc.attend === 1; - for (const pdetail of tsdoc.detail || []) { + for (const pdetail of tsdoc.journal || []) { psdict[pdetail.pid] = pdetail; rdict[pdetail.rid] = { _id: pdetail.rid }; } - if (this.canShowRecord(tdoc) && tsdoc.detail) { + if (this.canShowRecord(tdoc) && tsdoc.journal) { rdict = await record.getList( domainId, - tsdoc.detail.map((pdetail) => pdetail.rid), + tsdoc.journal.map((pdetail) => pdetail.rid), true, ); } @@ -94,9 +88,7 @@ class HomeworkDetailHandler extends HomeworkHandler { 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); - } + if (contest.isDone(tdoc)) throw new HomeworkNotLiveError(tdoc.docId); await contest.attend(domainId, tdoc.docId, this.user._id, document.TYPE_HOMEWORK); this.back(); } @@ -105,18 +97,16 @@ class HomeworkDetailHandler extends HomeworkHandler { class HomeworkDetailProblemHandler extends HomeworkHandler { async prepare({ domainId, tid, pid }) { this.checkPerm(PERM_VIEW_PROBLEM); - [this.tdoc, this.pdoc] = await Promise.all([ + [this.tdoc, this.pdoc, this.tsdoc] = await Promise.all([ contest.get(domainId, tid, document.TYPE_HOMEWORK), problem.get(domainId, pid, this.user._id), + contest.getStatus(domainId, tid, this.user._id, document.TYPE_HOMEWORK), ]); } 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; + const udoc = await user.getById(domainId, this.tdoc.owner); + const attended = this.tsdoc && this.tsdoc.attend === 1; if (!contest.isDone(this.tdoc)) { if (!attended) throw new HomeworkNotAttendedError(tid); if (!contest.isOngoing(this.tdoc)) throw new HomeworkNotLiveError(tid); @@ -129,41 +119,31 @@ class HomeworkDetailProblemHandler extends HomeworkHandler { ]; this.response.template = 'problem_detail.html'; this.response.body = { - tdoc: this.tdoc, pdoc: this.pdoc, tsdoc, udoc, attended, path, + tdoc: this.tdoc, pdoc: this.pdoc, tsdoc: this.tsdoc, udoc, attended, path, }; } } -class HomeworkDetailProblemSubmitHandler extends HomeworkHandler { +class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler { 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 udoc = await user.getById(domainId, this.tdoc.owner); + const attended = this.tsdoc && this.tsdoc.attend === 1; + 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 rdocs = this.canShowRecord(this.tdoc) + ? await record.getUserInProblemMulti(domainId, this.user._id, pid, true) + .sort('_id', -1).limit(10).toArray() : []; const path = [ ['homework_main', '/homework'], - [tdoc.title, `/homework/${tid}`, true], - [pdoc.title, `/homework/${tid}/p/${pid}`, true], + [this.tdoc.title, `/homework/${tid}`, true], + [this.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, + tdoc: this.tdoc, pdoc: this.pdoc, tsdoc: this.tsdoc, udoc, attended, path, rdocs, }; } @@ -220,11 +200,11 @@ class HomeworkCreateHandler extends HomeworkHandler { try { penaltySince = new Date(Date.parse(`${penaltySinceDate} ${penaltySinceTime.replace('-', ':')}`)); } catch (e) { - throw new ValidationError('end_at_date', 'end_at_time'); + throw new ValidationError('endAtDate', 'endAtTime'); } 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'); + if (beginAt >= penaltySince) { throw new ValidationError('endAtDate', 'endAtTime'); } + if (penaltySince > endAt) throw new ValidationError('extensionDays'); 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); @@ -253,7 +233,7 @@ class HomeworkEditHandler extends HomeworkHandler { datePenaltyText: tdoc.penaltySince.format('%Y-%m-%d'), timePenaltyText: tdoc.penaltySince.format('%H:%M'), extensionDays, - penaltyRules: _formatPenaltyRules(tdoc.penaltyRules), + penaltyRules: yaml.safeDump(tdoc.penaltyRules), pids: tdoc.pids.join(','), path, }; @@ -295,8 +275,25 @@ class HomeworkEditHandler extends HomeworkHandler { } } +class HomeworkScoreboardHandler extends HomeworkHandler { + async get({ domainId, tid }) { + const [tdoc, rows, udict] = await this.getScoreboard( + domainId, tid, false, document.TYPE_HOMEWORK, + ); + const path = [ + ['homework_main', '/homework'], + [tdoc.title, `/homework/${tid}`, true], + ['homework_scoreboard', null], + ]; + this.response.template = 'contest_scoreboard.html'; + this.response.body = { + tdoc, rows, path, udict, + }; + } +} + async function apply() { - contest.isHomeWorkExtended = (tdoc) => { + contest.isHomeworkExtended = (tdoc) => { const now = new Date().getTime(); return tdoc.penaltySince.getTime() <= now && now < tdoc.endAt.getTime(); }; @@ -342,46 +339,93 @@ async function apply() { detail, }; }, - showScoreboard(tdoc, now) { - return now > tdoc.endAt; - }, - showRecord(tdoc, now) { - return now > tdoc.endAt; - }, + showScoreboard: () => true, + showRecord: () => true, scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { const columns = [ { type: 'rank', value: _('Rank') }, { type: 'user', value: _('User') }, - { type: 'total_score', value: _('Total Score') }, + { type: 'total_score', value: _('Score') }, ]; - for (const i in tdoc.pids) { + if (isExport) { + columns.push( + { type: 'total_original_score', value: _('Original Score') }, + { type: 'total_time', value: _('Total Time (Seconds)') }, + ); + } + for (const index in tdoc.pids) { + const pid = tdoc.pids[index]; if (isExport) { - columns.push({ - type: 'problem_score', - value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title), - }); + columns.push( + { + type: 'problem_score', + value: '#{0} {1}'.format(index + 1, pdict[pid].title), + }, + { + type: 'problem_original_score', + value: '#{0} {1}'.format(index + 1, _('Original Score')), + }, + { + type: 'problem_time', + value: '#{0} {1}'.format(index + 1, _('Time (Seconds)')), + }, + { + type: 'problem_time_str', + value: '#{0} {1}'.format(index + 1, _('Time')), + }, + ); } else { columns.push({ type: 'problem_detail', - value: '#{0}'.format(i + 1), - raw: pdict[tdoc.pids[i]], + value: '#{0}'.format(index + 1), + raw: pdict[pid], }); } } const rows = [columns]; - for (const [rank, tsdoc] of rankedTsdocs) { + for (const rank in rankedTsdocs) { + const tsdoc = rankedTsdocs[rank]; 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 item of tsdoc.journal || []) { + tsddict[item.pid] = item; + } + const row = [ + { type: 'string', value: rank + 1 }, + { + type: 'user', + value: udict[tsdoc.uid].uname, + raw: udict[tsdoc.uid], + }, + { + type: 'string', + value: tsdoc.penaltyScore || 0, + }, + ]; + if (isExport) { + row.push({ type: 'string', value: tsdoc.score || 0 }); + row.push({ type: 'string', value: tsdoc.time || 0.0 }); + } + row.push({ type: 'string', value: misc.formatSeconds(tsdoc.time || 0) }); for (const pid of tdoc.pids) { - row.push({ - type: 'record', - value: (tsddict[pid] || {}).score || '-', - raw: (tsddict[pid] || {}).rid || null, - }); + const rdoc = (tsddict[pid] || {}).rid; + const colScore = (tsddict[pid] || {}).penaltyScore || '-'; + const colOriginalScore = (tsddict[pid] || {}).score || '-'; + const colTime = (tsddict[pid] || {}).time || '-'; + const colTimeStr = colTime !== '-' ? misc.formatSeconds(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); } @@ -392,6 +436,7 @@ async function apply() { Route('/homework', HomeworkMainHandler, PERM_VIEW_HOMEWORK); Route('/homework/create', HomeworkCreateHandler, PERM_CREATE_HOMEWORK); Route('/homework/:tid', HomeworkDetailHandler, PERM_VIEW_HOMEWORK); + Route('/homework/:tid/scoreboard', HomeworkScoreboardHandler, PERM_VIEW_HOMEWORK_SCOREBOARD); Route('/homework/:tid/p/:pid', HomeworkDetailProblemHandler, PERM_VIEW_HOMEWORK); Route('/homework/:tid/p/:pid/submit', HomeworkDetailProblemSubmitHandler, PERM_SUBMIT_PROBLEM); Route('/homework/:tid/edit', HomeworkEditHandler); @@ -436,24 +481,6 @@ class HomeworkCodeHandler(base.OperationHandler): 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): diff --git a/hydro/handler/judge.js b/hydro/handler/judge.js index 4ae3ab00e..1988915ca 100644 --- a/hydro/handler/judge.js +++ b/hydro/handler/judge.js @@ -16,7 +16,10 @@ async function _postJudge(rdoc) { const tasks = []; if (rdoc.tid) { tasks.push( - contest.updateStatus(rdoc.domainId, rdoc.tid, rdoc.uid, rdoc._id, rdoc.pid, accept, rdoc.score), + contest.updateStatus( + rdoc.domainId, rdoc.tid, rdoc.uid, + rdoc._id, rdoc.pid, accept, rdoc.score, rdoc.ttype, + ), ); } if (await problem.updateStatus(rdoc.pid, rdoc.uid, rdoc._id, rdoc.status)) { diff --git a/hydro/handler/manage.js b/hydro/handler/manage.js index da60ce92c..61409ad19 100644 --- a/hydro/handler/manage.js +++ b/hydro/handler/manage.js @@ -6,7 +6,7 @@ const { Route, Handler } = require('../service/server'); const { PERM_MANAGE } = require('../permission'); const hpm = require('../lib/hpm'); const loader = require('../loader'); -const { RoleAlreadyExistError, ValidationError } = require('../error'); +const { RoleAlreadyExistError, ValidationError, PermissionError } = require('../error'); class ManageHandler extends Handler { async prepare({ domainId }) { @@ -58,13 +58,16 @@ class ManageUserHandler extends ManageHandler { const uids = []; const rudocs = {}; const [udocs, roles] = await Promise.all([ - user.getMulti(domainId, { role: { $nin: ['default', 'guest'] } }).toArray(), + user.getInDomain(domainId), user.getRoles(domainId), ]); for (const role of roles) rudocs[role._id] = []; for (const udoc of udocs) { - uids.push(udoc._id); - rudocs[udoc.role].push(udoc); + uids.push(udoc.uid); + // TODO Improve here + // eslint-disable-next-line no-await-in-loop + const ud = await user.getById(domainId, udoc.uid); + rudocs[udoc.role].push({ ...ud, role: udoc.role }); } const rolesSelect = roles.map((role) => [role._id, role._id]); const udict = await user.getList(domainId, uids); @@ -79,17 +82,17 @@ class ManageUserHandler extends ManageHandler { }; } - async postSetUser({ uid, role }) { - await user.setRole(uid, role); + async postSetUser({ domainId, uid, role }) { + await user.setRole(domainId, uid, role); this.back(); } } class ManagePermissionHandler extends ManageHandler { async get({ domainId }) { - const [roles, sys] = await Promise.all([ + const [roles, d] = await Promise.all([ user.getRoles(domainId), - user.getById(domainId, 0), + domain.get(domainId), ]); const path = [ ['Hydro', '/'], @@ -97,27 +100,26 @@ class ManagePermissionHandler extends ManageHandler { ['manage_permission', null], ]; this.response.template = 'manage_permission.html'; - this.response.body = { roles, system: sys, path }; + this.response.body = { roles, domain: d, path }; } - async post({ roles }) { - for (const role of roles) { - let perms = ''; - for (const perm of roles) { - perms += perm; + async post({ domainId }) { + const roles = {}; + for (const role in this.request.body) { + if (this.request.body[role] instanceof Array) { + roles[role] = this.request.body[role].join(''); } - roles[role] = perms; } - await user.setRoles(roles); + await user.setRoles(domainId, roles); this.back(); } } class ManageRoleHandler extends ManageHandler { async get({ domainId }) { - const [roles, sys] = await Promise.all([ + const [roles, d] = await Promise.all([ user.getRoles(domainId), - user.getById(domainId, 0), + domain.get(domainId), ]); const path = [ ['Hydro', '/'], @@ -125,16 +127,16 @@ class ManageRoleHandler extends ManageHandler { ['manage_role', null], ]; this.response.template = 'manage_role.html'; - this.response.body = { roles, system: sys, path }; + this.response.body = { roles, domain: d, path }; } - async postAdd({ role }) { + async postAdd({ domainId, role }) { const [r, u] = await Promise.all([ - user.getRole(role), - user.getRole('default'), + user.getRole(domainId, role), + user.getRole(domainId, 'default'), ]); if (r) throw new RoleAlreadyExistError(role); - await user.addRole(role, u.perm); + await user.addRole(domainId, role, u.perm); this.back(); } @@ -149,7 +151,13 @@ class ManageRoleHandler extends ManageHandler { } } -class ManageModuleHandler extends ManageHandler { +class SystemHandler extends Handler { + async prepare() { + if (!this.user.priv) throw new PermissionError('???'); + } +} + +class SystemModuleHandler extends SystemHandler { async get() { const installed = await hpm.getInstalled(); const path = [ @@ -172,7 +180,7 @@ class ManageModuleHandler extends ManageHandler { } } -class ManageSettingHandler extends ManageHandler { +class SystemSettingHandler extends SystemHandler { async get() { this.response.template = 'manage_settings.html'; const current = {}; @@ -180,9 +188,7 @@ class ManageSettingHandler extends ManageHandler { for (const s of settings) { current[s.key] = await system.get(s.key); } - this.response.body = { - current, settings, - }; + this.response.body = { current, settings }; } async post(args) { @@ -208,8 +214,8 @@ async function apply() { Route('/manage/user', module.exports.ManageUserHandler); Route('/manage/permission', module.exports.ManagePermissionHandler); Route('/manage/role', module.exports.ManageRoleHandler); - Route('/manage/module', module.exports.ManageModuleHandler); - Route('/manage/setting', module.exports.ManageSettingHandler); + Route('/manage/module', SystemModuleHandler); + Route('/manage/setting', SystemSettingHandler); } global.Hydro.handler.manage = module.exports = { @@ -219,7 +225,7 @@ global.Hydro.handler.manage = module.exports = { ManageUserHandler, ManagePermissionHandler, ManageRoleHandler, - ManageModuleHandler, - ManageSettingHandler, + SystemModuleHandler, + SystemSettingHandler, apply, }; diff --git a/hydro/handler/record.js b/hydro/handler/record.js index c59c25531..02a21e9d4 100644 --- a/hydro/handler/record.js +++ b/hydro/handler/record.js @@ -12,10 +12,15 @@ const { Route, Handler, Connection, ConnectionHandler, } = require('../service/server'); -class RecordListHandler extends Handler { - async get({ domainId, page = 1 }) { +const RecordHandler = contest.ContestHandlerMixin(Handler); + +class RecordListHandler extends RecordHandler { + async get({ + domainId, page = 1, tid, uname_or_uid, + }) { this.response.template = 'record_main.html'; - const q = {}; + const q = { tid }; + if (uname_or_uid) q.$or = [{ unameLower: uname_or_uid.toLowerCase() }, { _id: parseInt(uname_or_uid) }]; const rdocs = await record.getMany(domainId, q, { _id: -1 }, page, await system.get('RECORD_PER_PAGE')); const [udict, pdict] = await Promise.all([ user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)), @@ -31,11 +36,14 @@ class RecordListHandler extends Handler { } } -class RecordDetailHandler extends Handler { +class RecordDetailHandler extends RecordHandler { async get({ domainId, rid }) { this.response.template = 'record_detail.html'; const rdoc = await record.get(domainId, rid); - if (rdoc.hidden) this.checkPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); + if (rdoc.tid) { + const tdoc = await contest.get(domainId, rdoc.tid, rdoc.ttype); + if (!this.canShowRecord(tdoc, true)) throw new PermissionError(rid); + } if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM_READ_RECORD_CODE)) { rdoc.code = null; } @@ -128,6 +136,9 @@ async function apply() { Route('/r', module.exports.RecordListHandler); Route('/r/:rid', module.exports.RecordDetailHandler); Route('/r/:rid/rejudge', module.exports.RecordRejudgeHandler); + Route('/record', module.exports.RecordListHandler); + Route('/record/:rid', module.exports.RecordDetailHandler); + Route('/record/:rid/rejudge', module.exports.RecordRejudgeHandler); Connection('/record-conn', module.exports.RecordConnectionHandler); Connection('/record-detail-conn', module.exports.RecordDetailConnectionHandler); } diff --git a/hydro/lib/misc.js b/hydro/lib/misc.js index 93fc0c043..9792f665d 100644 --- a/hydro/lib/misc.js +++ b/hydro/lib/misc.js @@ -49,6 +49,11 @@ function size(s, base = 1) { return '{0} {1}'.format(Math.round(s * unit), unitNames[unitNames.length - 1]); } +function formatSeconds(seconds) { + seconds = parseInt(seconds); + return '{:02}:{:02}:{:02}'.format(Math.floor(seconds / 3600), Math.floor((seconds % 3600) / 60), seconds % 60); +} + global.Hydro.lib.misc = module.exports = { - gravatar, datetimeSpan, paginate, size, + gravatar, datetimeSpan, paginate, size, formatSeconds, }; diff --git a/hydro/model/contest.js b/hydro/model/contest.js index 9c57fcded..58e6a53ca 100644 --- a/hydro/model/contest.js +++ b/hydro/model/contest.js @@ -74,7 +74,7 @@ const acm = { const rows = [columns]; for (const [rank, tsdoc] of rankedTsdocs) { const tsddict = {}; - if (tdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; } + if (tdoc.detail) { for (const item of tsdoc.journal) tsddict[item.pid] = item; } const row = []; row.push( { type: 'string', value: rank }, @@ -229,13 +229,8 @@ function add(domainId, title, content, owner, rule, * @returns {Tdoc} tdoc after modification */ 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'); } - if ($set.beginAt && $set.endAt) { - if ($set.beginAt >= $set.endAt) { - throw new ValidationError('beginAt', 'endAt'); - } + if ($set.rule) { + if (!this.RULES[$set.rule]) throw new ValidationError('rule'); } const tdoc = await document.get(domainId, type, tid); if (!tdoc) throw new ContestNotFoundError(tid); @@ -259,7 +254,7 @@ async function updateStatus( domainId, tid, uid, rid, pid, accept = false, score = 0, type = document.TYPE_CONTEST, ) { - await get(domainId, tid); + await get(domainId, tid, type); const tsdoc = await document.revPushStatus(domainId, type, tid, uid, 'journal', { rid, pid, accept, score, }); @@ -287,8 +282,8 @@ async function attend(domainId, tid, uid, type = document.TYPE_CONTEST) { return {}; } -function getMultiStatus(query) { - return document.getMultiStatus(query); +function getMultiStatus(domainId, query, docType = document.TYPE_CONTEST) { + return document.getMultiStatus(domainId, docType, query); } function isNew(tdoc, days = 1) { @@ -329,10 +324,10 @@ const ContestHandlerMixin = (c) => class extends c { return false; } - async getScoreboard(domainId, tid, isExport = false) { - const tdoc = await get(domainId, tid); + async getScoreboard(domainId, tid, isExport = false, docType = document.TYPE_CONTEST) { + const tdoc = await get(domainId, tid, docType); if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid); - const tsdocs = await getMultiStatus(domainId, tid) + const tsdocs = await getMultiStatus(domainId, tid, docType) .sort(RULES[tdoc.rule].statusSort).toArray(); const uids = []; for (const tsdoc of tsdocs) uids.push(tsdoc.uid); @@ -375,10 +370,10 @@ function _getStatusJournal(tsdoc) { async function recalcStatus(domainId, tid, type) { const [tdoc, tsdocs] = await Promise.all([ document.get(domainId, type, tid), - document.getMultiStatus(domainId, type, { docId: tid }), + document.getMultiStatus(domainId, type, { docId: tid }).toArray(), ]); const tasks = []; - for (const tsdoc of tsdocs) { + for (const tsdoc of tsdocs || []) { if (tsdoc.journal) { const journal = _getStatusJournal(tsdoc); const stats = RULES[tdoc.rule].stat(tdoc, journal); diff --git a/hydro/model/document.js b/hydro/model/document.js index c66f7bd29..5958fb474 100644 --- a/hydro/model/document.js +++ b/hydro/model/document.js @@ -47,7 +47,7 @@ function get(domainId, docType, docId) { async function set(domainId, docType, docId, args) { const res = await coll.findOneAndUpdate( { domainId, docType, docId }, - { $set: { args } }, + { $set: args }, { returnOriginal: false }, ); return res.value; diff --git a/hydro/model/record.js b/hydro/model/record.js index bebda80ff..09c505905 100644 --- a/hydro/model/record.js +++ b/hydro/model/record.js @@ -26,7 +26,19 @@ async function add(domainId, data) { judger: null, judgeAt: null, }); - const res = await coll.insertOne(data); + const [pdoc, res] = await Promise.all([ + problem.get(domainId, data.pid), + coll.insertOne(data), + ]); + await task.add({ + type: 'judge', + rid: res.insertedId, + domainId, + pid: data.pid, + data: pdoc.data, + lang: data.lang, + code: data.code, + }); return res.insertedId; } @@ -35,11 +47,10 @@ async function add(domainId, data) { * @param {ObjectID} rid * @returns {import('../interface').Record} */ -async function get(domainId, rid, showHidden = false) { +async function get(domainId, rid) { 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; } diff --git a/hydro/model/user.js b/hydro/model/user.js index 8b36d96d4..4d1bbd197 100644 --- a/hydro/model/user.js +++ b/hydro/model/user.js @@ -16,6 +16,8 @@ class USER { this.salt = () => user.salt; this.hash = () => user.hash; this.perm = user.perm; + this.priv = user.priv; + this.role = user.role; this.viewLang = user.language || 'zh_CN'; this.codeLang = user.codeLang || 'c'; this.codeTemplate = user.codeTemplate || ''; @@ -161,6 +163,16 @@ function setRole(domainId, uid, role) { return document.setStatus(domainId, document.TYPE_DOMAIN_ROLE, 0, uid, { role }); } +function setRoles(domainId, roles) { + const tasks = []; + for (const role in roles) { + tasks.push(document.set( + domainId, document.TYPE_DOMAIN_ROLE, role, { content: roles[role] }, + )); + } + return Promise.all(tasks); +} + async function getRoles(domainId) { const docs = await document.getMulti(domainId, document.TYPE_DOMAIN_ROLE).sort('_id', 1).toArray(); const roles = []; @@ -174,6 +186,10 @@ function getRole(domainId, name) { return document.get(domainId, document.TYPE_DOMAIN_ROLE, name); } +function getInDomain(domainId) { + return document.getMultiStatus(domainId, document.TYPE_DOMAIN_ROLE).toArray(); +} + function addRole(domainId, name, permission) { return document.add(domainId, permission, 1, document.TYPE_DOMAIN_ROLE, name); } @@ -205,9 +221,11 @@ global.Hydro.model.user = module.exports = { setPassword, getPrefixList, setRole, + setRoles, getRole, getList, getRoles, + getInDomain, addRole, deleteRoles, ensureIndexes, diff --git a/hydro/permission.js b/hydro/permission.js index 9f80e3e70..f26f63c7f 100644 --- a/hydro/permission.js +++ b/hydro/permission.js @@ -40,6 +40,8 @@ const perm = { PERM_CREATE_HOMEWORK: 'f', PERM_EDIT_HOMEWORK: 'g', PERM_ATTEND_HOMEWORK: 'h', + PERM_VIEW_HOMEWORK_SCOREBOARD: 'i', + PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD: 'j', PERM_JUDGE: 'd', }; @@ -83,9 +85,12 @@ const PERMS = [ Permission('perm_contest', perm.PERM_CREATE_CONTEST, 'Create contests'), Permission('perm_contest', perm.PERM_ATTEND_CONTEST, 'Attend contests'), Permission('perm_contest', perm.PERM_EDIT_CONTEST, 'Edit any contests'), - Permission('perm_training', perm.PERM_VIEW_TRAINING, 'View training plans'), - Permission('perm_training', perm.PERM_CREATE_TRAINING, 'Create training plans'), - Permission('perm_training', perm.PERM_EDIT_TRAINING, 'Edit training plans'), + Permission('perm_homework', perm.PERM_VIEW_HOMEWORK, 'View homework'), + Permission('perm_homework', perm.PERM_VIEW_HOMEWORK_SCOREBOARD, 'View homework scoreboard'), + Permission('perm_homework', perm.PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD, 'View hidden homework submission status and scoreboard'), + Permission('perm_homework', perm.PERM_CREATE_HOMEWORK, 'Create homework'), + Permission('perm_homework', perm.PERM_ATTEND_HOMEWORK, 'Claim homework'), + Permission('perm_homework', perm.PERM_EDIT_HOMEWORK, 'Edit any homework'), ]; const PERMS_BY_FAMILY = {}; @@ -101,7 +106,8 @@ perm.PERM_BASIC = perm.PERM_VIEW_PROBLEM + perm.PERM_VIEW_TRAINING + perm.PERM_VIEW_CONTEST + perm.PERM_VIEW_CONTEST_SCOREBOARD - + perm.PERM_VIEW_HOMEWORK; + + perm.PERM_VIEW_HOMEWORK + + perm.PERM_VIEW_HOMEWORK_SCOREBOARD; perm.PERM_DEFAULT = perm.PERM_BASIC + perm.PERM_SUBMIT_PROBLEM diff --git a/hydro/service/server.js b/hydro/service/server.js index 7589c4356..4df0421ca 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -44,7 +44,7 @@ const validate = { pid: (pid) => (Number.isSafeInteger(parseInt(pid)) ? parseInt(pid) : pid), content: validator.checkContent, title: validator.checkTitle, - uid: parseInt(validator.checkUid), + uid: (uid) => parseInt(validator.checkUid(uid)), password: validator.checkPassword, mail: validator.checkEmail, uname: validator.checkUname, @@ -58,12 +58,14 @@ const validate = { 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; + pids: (pids) => { + const res = pids.split(',').map((i) => i.trim()); + for (const i in res) { + if (Number.isSafeInteger(parseInt(res[i]))) res[i] = parseInt(res[i]); + } + return res; }, + role: validator.checkRole, penaltyRules: (penaltyRules) => { try { penaltyRules = yaml.safeLoad(penaltyRules); diff --git a/templates/components/problem.html b/templates/components/problem.html index d950e476c..3b74e05d6 100644 --- a/templates/components/problem.html +++ b/templates/components/problem.html @@ -1,8 +1,8 @@ -{% macro render_problem_title(pdoc, tdoc=none, show_tags=true, show_invisible_flag=true, rp=none, invalid=false) %} +{% macro render_problem_title(pdoc, tdoc=none, prefix='/c', show_tags=true, show_invisible_flag=true, rp=none, invalid=false) %} {%- if not invalid %} {{ _('Submitted') }} +