diff --git a/hydro/model/contest.js b/hydro/model/contest.js index 6973f96f4..5605b03b4 100644 --- a/hydro/model/contest.js +++ b/hydro/model/contest.js @@ -6,12 +6,190 @@ const { } = require('../error'); const { PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD } = require('../permission'); const validator = require('../lib/validator'); +const ranked = require('../lib/rank'); const db = require('../service/db'); const coll = db.collection('contest'); const collStatus = db.collection('contest.status'); -const RULES = {}; +const acm = { + TEXT: 'ACM/ICPC', + check: () => { }, + showScoreboard: () => true, + showRecord: (tdoc, now) => now > tdoc.endAt, + stat: (tdoc, journal) => { + const naccept = {}; + const effective = {}; + const detail = []; + let accept = 0; + let time = 0; + for (const j in journal) { + if (tdoc.pids.includes(j.pid) + && !(effective.includes(j.pid) && effective[j.pid].accept)) { + effective[j.pid] = j; + } + if (!j.accept) naccept[j.pid]++; + } + function _time(jdoc) { + const real = jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000); + const penalty = 20 * 60 * naccept[jdoc.pid]; + return real + penalty; + } + for (const j of effective) detail.push({ ...j, naccept: naccept[j.pid], time: _time(j) }); + for (const d of detail) { + accept += d.accept; + if (d.accept) time += d.time; + } + return { accept, time, detail }; + }, + scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { + const columns = [ + { type: 'rank', value: _('Rank') }, + { type: 'user', value: _('User') }, + { type: 'solved_problems', value: _('Solved Problems') }, + ]; + if (isExport) { + columns.push({ type: 'total_time', value: _('Total Time (Seconds)') }); + columns.push({ type: 'total_time_str', value: _('Total Time') }); + } + for (const i in tdoc.pids) { + if (isExport) { + columns.push({ + type: 'problem_flag', + value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title), + }); + columns.push({ + type: 'problem_time', + value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')), + }); + columns.push({ + type: 'problem_time_str', + value: '#{0} {1}'.format(i + 1, _('Time')), + }); + } else { + columns.push({ + type: 'problem_detail', + value: '#{0}'.format(i + 1), + raw: pdict[tdoc.pids[i]], + }); + } + } + const rows = [columns]; + for (const [rank, tsdoc] of rankedTsdocs) { + const tsddict = {}; + if (tdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; } + const row = []; + row.push( + { type: 'string', value: rank }, + { type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }, + { type: 'string', value: tsdoc.accept || 0 }, + ); + if (isExport) { + row.push( + { type: 'string', value: tsdoc.time || 0.0 }, + { type: 'string', value: tsdoc.time || 0.0 }, + ); + } + for (const pid of tdoc.pids) { + let rdoc; + let colAccepted; + let colTime; + let colTimeStr; + if ((tsddict[pid] || {}).accept) { + rdoc = tsddict[pid].rid; + colAccepted = _('Accepted'); + colTime = tsddict[pid].time; + colTimeStr = colTime; + } else { + rdoc = null; + colAccepted = '-'; + colTime = '-'; + colTimeStr = '-'; + } + if (isExport) { + row.push({ type: 'string', value: colAccepted }); + row.push({ type: 'string', value: colTime }); + row.push({ type: 'string', value: colTimeStr }); + } else { + row.push({ + type: 'record', + value: '{0}\n{1}'.format(colAccepted, colTimeStr), + raw: rdoc, + }); + } + rows.push(row); + } + } + return rows; + }, +}; + +const oi = { + TEXT: 'OI', + check: () => { }, + stat: (tdoc, journal) => { + const detail = {}; + let score = 0; + for (const j in journal) { + if (tdoc.pids.includes(j.pid)) { + detail[j.pid] = j; + score += j.score; + } + } + return { score, detail }; + }, + showScoreboard(tdoc, now) { + return now > tdoc.endAt; + }, + showRecord(tdoc, now) { + return now > tdoc.endAt; + }, + scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { + const columns = [ + { type: 'rank', value: _('Rank') }, + { type: 'user', value: _('User') }, + { type: 'total_score', value: _('Total Score') }, + ]; + for (const i in tdoc.pids) { + if (isExport) { + columns.push({ + type: 'problem_score', + value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title), + }); + } else { + columns.push({ + type: 'problem_detail', + value: '#{0}'.format(i + 1), + raw: pdict[tdoc.pids[i]], + }); + } + } + const rows = [columns]; + for (const [rank, tsdoc] of rankedTsdocs) { + const tsddict = {}; + if (tsdoc.journal) { for (const item of tsdoc.journal) tsddict[item.pid] = item; } + const row = []; + row.push({ type: 'string', value: rank }); + row.push({ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }); + row.push({ type: 'string', value: tsdoc.score || 0 }); + for (const pid of tdoc.pids) { + row.push({ + type: 'record', + value: (tsddict[pid] || {}).score || '-', + raw: (tsddict[pid] || {}).rid || null, + }); + } + rows.push(row); + } + return rows; + }, + rank: (tdocs) => ranked(tdocs, (a, b) => a.score === b.score), +}; + + +const RULES = { + acm, oi, +}; /** * @typedef {import('bson').ObjectID} ObjectID @@ -164,7 +342,7 @@ const ContestHandlerMixin = (c) => class extends c { } }; -module.exports = { +global.Hydro['model.contest'] = module.exports = { RULES, ContestHandlerMixin, add, @@ -198,8 +376,6 @@ module.exports = { : 'finished'), }; -global.Hydro['model.contest'] = module.exports; - /* def _get_status_journal(tsdoc): diff --git a/module/contest-rule-basic/acm.js b/module/contest-rule-basic/acm.js deleted file mode 100644 index b6c8d55f8..000000000 --- a/module/contest-rule-basic/acm.js +++ /dev/null @@ -1,111 +0,0 @@ -module.exports = { - TEXT: 'ACM/ICPC', - check: () => { }, - showScoreboard: () => true, - showRecord: (tdoc, now) => now > tdoc.endAt, - stat: (tdoc, journal) => { - const naccept = {}; - const effective = {}; - const detail = []; - let accept = 0; - let time = 0; - for (const j in journal) { - if (tdoc.pids.includes(j.pid) - && !(effective.includes(j.pid) && effective[j.pid].accept)) { - effective[j.pid] = j; - } - if (!j.accept) naccept[j.pid]++; - } - function _time(jdoc) { - const real = jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000); - const penalty = 20 * 60 * naccept[jdoc.pid]; - return real + penalty; - } - for (const j of effective) detail.push({ ...j, naccept: naccept[j.pid], time: _time(j) }); - for (const d of detail) { - accept += d.accept; - if (d.accept) time += d.time; - } - return { accept, time, detail }; - }, - scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { - const columns = [ - { type: 'rank', value: _('Rank') }, - { type: 'user', value: _('User') }, - { type: 'solved_problems', value: _('Solved Problems') }, - ]; - if (isExport) { - columns.push({ type: 'total_time', value: _('Total Time (Seconds)') }); - columns.push({ type: 'total_time_str', value: _('Total Time') }); - } - for (const i in tdoc.pids) { - if (isExport) { - columns.push({ - type: 'problem_flag', - value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title), - }); - columns.push({ - type: 'problem_time', - value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')), - }); - columns.push({ - type: 'problem_time_str', - value: '#{0} {1}'.format(i + 1, _('Time')), - }); - } else { - columns.push({ - type: 'problem_detail', - value: '#{0}'.format(i + 1), - raw: pdict[tdoc.pids[i]], - }); - } - } - const rows = [columns]; - for (const [rank, tsdoc] of rankedTsdocs) { - const tsddict = {}; - if (tdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; } - const row = []; - row.push( - { type: 'string', value: rank }, - { type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }, - { type: 'string', value: tsdoc.accept || 0 }, - ); - if (isExport) { - row.push( - { type: 'string', value: tsdoc.time || 0.0 }, - { type: 'string', value: tsdoc.time || 0.0 }, - ); - } - for (const pid of tdoc.pids) { - let rdoc; - let colAccepted; - let colTime; - let colTimeStr; - if ((tsddict[pid] || {}).accept) { - rdoc = tsddict[pid].rid; - colAccepted = _('Accepted'); - colTime = tsddict[pid].time; - colTimeStr = colTime; - } else { - rdoc = null; - colAccepted = '-'; - colTime = '-'; - colTimeStr = '-'; - } - if (isExport) { - row.push({ type: 'string', value: colAccepted }); - row.push({ type: 'string', value: colTime }); - row.push({ type: 'string', value: colTimeStr }); - } else { - row.push({ - type: 'record', - value: '{0}\n{1}'.format(colAccepted, colTimeStr), - raw: rdoc, - }); - } - rows.push(row); - } - } - return rows; - }, -}; diff --git a/module/contest-rule-basic/hydro.json b/module/contest-rule-basic/hydro.json deleted file mode 100644 index f1f7c1d84..000000000 --- a/module/contest-rule-basic/hydro.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "contest-rule-basic", - "version": "1.0.0", - "description": "Provide contest rules: ACM/ICPC and OI." -} \ No newline at end of file diff --git a/module/contest-rule-basic/locale/zh_CN.yaml b/module/contest-rule-basic/locale/zh_CN.yaml deleted file mode 100644 index 102f39267..000000000 --- a/module/contest-rule-basic/locale/zh_CN.yaml +++ /dev/null @@ -1 +0,0 @@ -'Provide contest rules: ACM/ICPC and OI.': 提供ACM和OI赛制。 \ No newline at end of file diff --git a/module/contest-rule-basic/model.js b/module/contest-rule-basic/model.js deleted file mode 100644 index 77d995248..000000000 --- a/module/contest-rule-basic/model.js +++ /dev/null @@ -1,6 +0,0 @@ -const acm = require('./acm'); -const oi = require('./oi'); - -const rules = global.Hydro['model.contest'].RULES; -rules.oi = oi; -rules.acm = acm; diff --git a/module/contest-rule-basic/oi.js b/module/contest-rule-basic/oi.js deleted file mode 100644 index 1e8813cb1..000000000 --- a/module/contest-rule-basic/oi.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @type {import('../../hydro/lib/rank')} - */ -const ranked = global.Hydro['lib.rank']; - -module.exports = { - TEXT: 'OI', - check: () => { }, - stat: (tdoc, journal) => { - const detail = {}; - let score = 0; - for (const j in journal) { - if (tdoc.pids.includes(j.pid)) { - detail[j.pid] = j; - score += j.score; - } - } - return { score, detail }; - }, - showScoreboard(tdoc, now) { - return now > tdoc.endAt; - }, - showRecord(tdoc, now) { - return now > tdoc.endAt; - }, - scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { - const columns = [ - { type: 'rank', value: _('Rank') }, - { type: 'user', value: _('User') }, - { type: 'total_score', value: _('Total Score') }, - ]; - for (const i in tdoc.pids) { - if (isExport) { - columns.push({ - type: 'problem_score', - value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title), - }); - } else { - columns.push({ - type: 'problem_detail', - value: '#{0}'.format(i + 1), - raw: pdict[tdoc.pids[i]], - }); - } - } - const rows = [columns]; - for (const [rank, tsdoc] of rankedTsdocs) { - const tsddict = {}; - if (tsdoc.journal) { for (const item of tsdoc.journal) tsddict[item.pid] = item; } - const row = []; - row.push({ type: 'string', value: rank }); - row.push({ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }); - row.push({ type: 'string', value: tsdoc.score || 0 }); - for (const pid of tdoc.pids) { - row.push({ - type: 'record', - value: (tsddict[pid] || {}).score || '-', - raw: (tsddict[pid] || {}).rid || null, - }); - } - rows.push(row); - } - return rows; - }, - rank: (tdocs) => ranked(tdocs, (a, b) => a.score === b.score), -}; diff --git a/module/wiki/raw/development/basic.md b/module/wiki/raw/development/basic.md index b31a35a4e..d51ff674d 100644 --- a/module/wiki/raw/development/basic.md +++ b/module/wiki/raw/development/basic.md @@ -2,3 +2,11 @@ Hydro 后端使用 NodeJS 编写,前端使用 JQuery + React。 代码风格遵循 airbnb 标准(详见 .eslintrc.js)。 + +由于模块中不能使用 require() 引入 Hydro 的文件,因此需要从 global.Hydro 中取出需要的模块。 +例: + +```js +const { db } = global.Hydro.service; +const { problem } = global.Hydro.model; +``` diff --git a/module/wiki/raw/development/contents.json b/module/wiki/raw/development/contents.json index 4c2610c37..439a1cae8 100644 --- a/module/wiki/raw/development/contents.json +++ b/module/wiki/raw/development/contents.json @@ -1,8 +1,11 @@ [ "basic", "module", + "hydro", "handler", "service", + "lib", "file", + "locale", "template" ] \ No newline at end of file diff --git a/module/wiki/raw/development/hydro.md b/module/wiki/raw/development/hydro.md new file mode 100644 index 000000000..6872e2904 --- /dev/null +++ b/module/wiki/raw/development/hydro.md @@ -0,0 +1,11 @@ +# Hydro.json + +Hydro.json 是模块的声明文件,格式如下。 + +```json +{ + "id": "模块ID", + "version": "模块版本", + "description": "模块描述" +} +``` diff --git a/module/wiki/raw/development/lib.md b/module/wiki/raw/development/lib.md new file mode 100644 index 000000000..4cdd0ff62 --- /dev/null +++ b/module/wiki/raw/development/lib.md @@ -0,0 +1,3 @@ +# Lib + +库文件。通常用于提供一些功能(废话)。 diff --git a/module/wiki/raw/development/locale.md b/module/wiki/raw/development/locale.md new file mode 100644 index 000000000..e97db3123 --- /dev/null +++ b/module/wiki/raw/development/locale.md @@ -0,0 +1,3 @@ +# Locale + +用于提供多国翻译。格式与 Hydro 的 locale 文件夹格式相同。 diff --git a/module/wiki/raw/development/template.md b/module/wiki/raw/development/template.md index 73676252b..b75f6f517 100644 --- a/module/wiki/raw/development/template.md +++ b/module/wiki/raw/development/template.md @@ -1,5 +1,31 @@ # Template -页面模板。 +页面模板,使用 nunjucks 语法。 +传入了 _ 翻译函数与 model 等。 -TODO \ No newline at end of file +```html +{% set page_name = "page_name" %} +{% extends "layout/basic.html" %} +{% block content %} +
+
+
+
+
+

{{ _('Oops!') }}

+

{{ _(error.message).format(error.params) }}

+

{{ _('Technical Information') }}:

+

{{ _('Type') }}: {{ error.code }}

+

{{ _('Arguments') }}: +

    + {% for param in error.params %} +
  1. {{ param }}
  2. + {% endfor %} +
+

+
+
+{% endblock %} +``` + +
请不要覆盖已有模板。
\ No newline at end of file