mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
模块开发指南之模板编写
This commit is contained in:
1 parent
b6dce8a030
commit
747cb1b9b5
12 files changed
+236
-195
No files matched your search
+180
-4
@@ -6,12 +6,190 @@ const {
|
|||||||
} = require('../error');
|
} = require('../error');
|
||||||
const { PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD } = require('../permission');
|
const { PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD } = require('../permission');
|
||||||
const validator = require('../lib/validator');
|
const validator = require('../lib/validator');
|
||||||
|
const ranked = require('../lib/rank');
|
||||||
const db = require('../service/db');
|
const db = require('../service/db');
|
||||||
|
|
||||||
const coll = db.collection('contest');
|
const coll = db.collection('contest');
|
||||||
const collStatus = db.collection('contest.status');
|
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
|
* @typedef {import('bson').ObjectID} ObjectID
|
||||||
@@ -164,7 +342,7 @@ const ContestHandlerMixin = (c) => class extends c {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
global.Hydro['model.contest'] = module.exports = {
|
||||||
RULES,
|
RULES,
|
||||||
ContestHandlerMixin,
|
ContestHandlerMixin,
|
||||||
add,
|
add,
|
||||||
@@ -198,8 +376,6 @@ module.exports = {
|
|||||||
: 'finished'),
|
: 'finished'),
|
||||||
};
|
};
|
||||||
|
|
||||||
global.Hydro['model.contest'] = module.exports;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
def _get_status_journal(tsdoc):
|
def _get_status_journal(tsdoc):
|
||||||
|
|||||||
@@ -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;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "contest-rule-basic",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Provide contest rules: ACM/ICPC and OI."
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
'Provide contest rules: ACM/ICPC and OI.': 提供ACM和OI赛制。
|
|
||||||
@@ -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;
|
|
||||||
@@ -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),
|
|
||||||
};
|
|
||||||
@@ -2,3 +2,11 @@
|
|||||||
|
|
||||||
Hydro 后端使用 NodeJS 编写,前端使用 JQuery + React。
|
Hydro 后端使用 NodeJS 编写,前端使用 JQuery + React。
|
||||||
代码风格遵循 airbnb 标准(详见 .eslintrc.js)。
|
代码风格遵循 airbnb 标准(详见 .eslintrc.js)。
|
||||||
|
|
||||||
|
由于模块中不能使用 require() 引入 Hydro 的文件,因此需要从 global.Hydro 中取出需要的模块。
|
||||||
|
例:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { db } = global.Hydro.service;
|
||||||
|
const { problem } = global.Hydro.model;
|
||||||
|
```
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
[
|
[
|
||||||
"basic",
|
"basic",
|
||||||
"module",
|
"module",
|
||||||
|
"hydro",
|
||||||
"handler",
|
"handler",
|
||||||
"service",
|
"service",
|
||||||
|
"lib",
|
||||||
"file",
|
"file",
|
||||||
|
"locale",
|
||||||
"template"
|
"template"
|
||||||
]
|
]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Hydro.json
|
||||||
|
|
||||||
|
Hydro.json 是模块的声明文件,格式如下。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "模块ID",
|
||||||
|
"version": "模块版本",
|
||||||
|
"description": "模块描述"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Lib
|
||||||
|
|
||||||
|
库文件。通常用于提供一些功能(废话)。
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Locale
|
||||||
|
|
||||||
|
用于提供多国翻译。格式与 Hydro 的 locale 文件夹格式相同。
|
||||||
@@ -1,5 +1,31 @@
|
|||||||
# Template
|
# Template
|
||||||
|
|
||||||
页面模板。
|
页面模板,使用 nunjucks 语法。
|
||||||
|
传入了 _ 翻译函数与 model 等。
|
||||||
|
|
||||||
TODO
|
```html
|
||||||
|
{% set page_name = "page_name" %}
|
||||||
|
{% extends "layout/basic.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="error__container clearfix">
|
||||||
|
<div class="error__icon-container">
|
||||||
|
<div class="error__twd2"></div>
|
||||||
|
</div>
|
||||||
|
<div class="error__text-container">
|
||||||
|
<h1>{{ _('Oops!') }}</h1>
|
||||||
|
<p>{{ _(error.message).format(error.params) }}</p>
|
||||||
|
<p>{{ _('Technical Information') }}:</p>
|
||||||
|
<p>{{ _('Type') }}: {{ error.code }}</p>
|
||||||
|
<p>{{ _('Arguments') }}:
|
||||||
|
<ol>
|
||||||
|
{% for param in error.params %}
|
||||||
|
<li>{{ param }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
<blockquote class="note">请不要覆盖已有模板。</blockquote>
|
||||||
Reference in new issue
Block a user