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
e06283c0cc
commit
97b0d782b9
24 files changed
+465
-104
No files matched your search
@@ -3,8 +3,49 @@
|
|||||||
Hydro是一个高效的信息学在线测评系统。
|
Hydro是一个高效的信息学在线测评系统。
|
||||||
特点: 易于部署,轻量,功能强大。
|
特点: 易于部署,轻量,功能强大。
|
||||||
|
|
||||||
|
Hydro 的界面基于 Vijos 二次开发。
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
使用 `docker-compose`(推荐)
|
#### 使用 `docker-compose`(推荐)
|
||||||
|
|
||||||
// TODO(WIP)
|
// TODO(WIP)
|
||||||
|
|
||||||
|
#### 直接部署:
|
||||||
|
|
||||||
|
安装mongodb (省略)
|
||||||
|
编辑 `config.yaml` :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
db:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 27017
|
||||||
|
name: hydro
|
||||||
|
username: hydro
|
||||||
|
password: db.drop();
|
||||||
|
listen:
|
||||||
|
host: 127.0.0.1
|
||||||
|
port: 8888
|
||||||
|
session:
|
||||||
|
domain: '*'
|
||||||
|
```
|
||||||
|
|
||||||
|
运行:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn
|
||||||
|
cd ui
|
||||||
|
yarn
|
||||||
|
yarn build:production
|
||||||
|
cd ..
|
||||||
|
node hydro/development.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## 鸣谢
|
||||||
|
|
||||||
|
排名不分先后,按照链接字典序
|
||||||
|
|
||||||
|
- [Github](https://github.com/) 为 Hydro 提供了代码托管与自动构建。
|
||||||
|
- [criyle](https://github.com/criyle) 提供评测沙箱实现。
|
||||||
|
- [Vijos](https://github.com/vijos/vj4) 为 Hydro 提供了UI框架。
|
||||||
|
- [undefined](https://masnn.io:38443/) 项目主要开发人员。
|
||||||
@@ -34,6 +34,7 @@ async function run() {
|
|||||||
require('./handler/judge');
|
require('./handler/judge');
|
||||||
require('./handler/user');
|
require('./handler/user');
|
||||||
require('./handler/contest');
|
require('./handler/contest');
|
||||||
|
require('./handler/training');
|
||||||
server.start();
|
server.start();
|
||||||
}
|
}
|
||||||
process.on('restart', async () => {
|
process.on('restart', async () => {
|
||||||
|
|||||||
+7
-6
@@ -118,6 +118,12 @@ class ContestScoreboardHiddenError extends ForbiddenError {
|
|||||||
this.params = [tid];
|
this.params = [tid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class TrainingAlreadyEnrollError extends ForbiddenError {
|
||||||
|
constructor(tid, uid) {
|
||||||
|
super("You've already enrolled this training.");
|
||||||
|
this.params = [tid, uid];
|
||||||
|
}
|
||||||
|
}
|
||||||
class ProblemNotFoundError extends NotFoundError {
|
class ProblemNotFoundError extends NotFoundError {
|
||||||
constructor(pid) {
|
constructor(pid) {
|
||||||
super('ProblemNotFoundError');
|
super('ProblemNotFoundError');
|
||||||
@@ -180,6 +186,7 @@ module.exports = {
|
|||||||
ContestAlreadyAttendedError,
|
ContestAlreadyAttendedError,
|
||||||
UserFacingError,
|
UserFacingError,
|
||||||
SystemError,
|
SystemError,
|
||||||
|
TrainingAlreadyEnrollError,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -337,12 +344,6 @@ class TrainingRequirementNotSatisfiedError(ForbiddenError):
|
|||||||
return 'Training requirement is not satisfied.'
|
return 'Training requirement is not satisfied.'
|
||||||
|
|
||||||
|
|
||||||
class TrainingAlreadyEnrollError(ForbiddenError):
|
|
||||||
@property
|
|
||||||
def message(self):
|
|
||||||
return "You've already enrolled this training."
|
|
||||||
|
|
||||||
|
|
||||||
class UsageExceededError(ForbiddenError):
|
class UsageExceededError(ForbiddenError):
|
||||||
@property
|
@property
|
||||||
def message(self):
|
def message(self):
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ class ProblemSolutionReplyRawHandler extends ProblemDetailHandler {
|
|||||||
async get({ psid }) {
|
async get({ psid }) {
|
||||||
this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION);
|
this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION);
|
||||||
const [psdoc, psrdoc] = await solution.getReply(psid);
|
const [psdoc, psrdoc] = await solution.getReply(psid);
|
||||||
if ((!psdoc) || psdoc.pid != this.pdoc._id) throw new SolutionNotFoundError(psid);
|
if ((!psdoc) || psdoc.pid !== this.pdoc._id) throw new SolutionNotFoundError(psid);
|
||||||
this.response.type = 'text/markdown';
|
this.response.type = 'text/markdown';
|
||||||
this.response.body = psrdoc.content;
|
this.response.body = psrdoc.content;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ class RecordDetailHandler extends Handler {
|
|||||||
this.response.template = 'record_detail.html';
|
this.response.template = 'record_detail.html';
|
||||||
const rdoc = await record.get(rid);
|
const rdoc = await record.get(rid);
|
||||||
if (rdoc.hidden) this.checkPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
if (rdoc.hidden) this.checkPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
|
||||||
if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM_READ_RECORD_CODE)) rdoc.code = null;
|
if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM_READ_RECORD_CODE)) {
|
||||||
|
rdoc.code = null;
|
||||||
|
}
|
||||||
this.response.body = {
|
this.response.body = {
|
||||||
path: [
|
path: [
|
||||||
['Hydro', '/'],
|
['Hydro', '/'],
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
const assert = require('assert');
|
||||||
|
const { ValidationError, ProblemNotFoundError } = require('../error');
|
||||||
|
const {
|
||||||
|
PERM_LOGGEDIN, PERM_VIEW_TRAINING, PERM_VIEW_PROBLEM_HIDDEN,
|
||||||
|
PERM_CREATE_TRAINING, PERM_EDIT_TRAINING,
|
||||||
|
} = require('../permission');
|
||||||
|
const { constants } = require('../options');
|
||||||
|
const paginate = require('../lib/paginate');
|
||||||
|
const problem = require('../model/problem');
|
||||||
|
const builtin = require('../model/builtin');
|
||||||
|
const training = require('../model/training');
|
||||||
|
const user = require('../model/user');
|
||||||
|
const { Route, Handler } = require('../service/server');
|
||||||
|
|
||||||
|
async function _parseDagJson(dag) {
|
||||||
|
const parsed = [];
|
||||||
|
try {
|
||||||
|
dag = JSON.parse(dag);
|
||||||
|
assert(dag instanceof Array, 'dag must be an array');
|
||||||
|
const ids = new Set(dag.map((s) => s._id));
|
||||||
|
assert(dag.length === ids.size, '_id must be unique');
|
||||||
|
for (const node of dag) {
|
||||||
|
assert(node._id, 'each node should have a _id');
|
||||||
|
assert(node.title, 'each node shoule have a title');
|
||||||
|
assert(node.requireNids instanceof Array);
|
||||||
|
assert(node.pids instanceof Array);
|
||||||
|
assert(node.pids.length);
|
||||||
|
for (const nid of node.requireNids) {
|
||||||
|
assert(ids.has(nid), `required nid ${nid} not found`);
|
||||||
|
}
|
||||||
|
for (const i in node.pids) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const pdoc = await problem.get(node.pids[i]); // FIXME no-await-in-loop
|
||||||
|
assert(pdoc, `Problem not found: ${node.pids[i]}`);
|
||||||
|
node.pids[i] = pdoc._id;
|
||||||
|
}
|
||||||
|
const newNode = {
|
||||||
|
_id: parseInt(node._id),
|
||||||
|
title: node.title,
|
||||||
|
requireNids: Array.from(new Set(node.requireNids)),
|
||||||
|
pids: Array.from(new Set(node.pids)),
|
||||||
|
};
|
||||||
|
parsed.push(newNode);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw new ValidationError('dag');
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrainingHandler extends Handler {
|
||||||
|
async _prepare() {
|
||||||
|
this.checkPerm(PERM_VIEW_TRAINING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrainingMainHandler extends TrainingHandler {
|
||||||
|
async get({ sort, page }) {
|
||||||
|
const qs = sort ? 'sort={0}'.format(sort) : '';
|
||||||
|
const [tdocs, tpcount] = await paginate(
|
||||||
|
training.getMulti().sort('_id', 1),
|
||||||
|
page,
|
||||||
|
constants.TRAINING_PER_PAGE,
|
||||||
|
);
|
||||||
|
const tids = new Set();
|
||||||
|
for (const tdoc of tdocs) tids.add(tdoc._id);
|
||||||
|
const tsdict = {};
|
||||||
|
let tdict = {};
|
||||||
|
if (this.user.hasPerm(PERM_LOGGEDIN)) {
|
||||||
|
const enrolledTids = new Set();
|
||||||
|
const tsdocs = await training.getMultiStatus({
|
||||||
|
uid: this.user._id,
|
||||||
|
$or: [{ _id: { $in: Array.from(tids) } }, { enroll: 1 }],
|
||||||
|
}).toArray();
|
||||||
|
for (const tsdoc of tsdocs) {
|
||||||
|
tsdict[tsdoc._id] = tsdoc;
|
||||||
|
enrolledTids.add(tsdoc._id);
|
||||||
|
}
|
||||||
|
for (const tid of tids) enrolledTids.delete(tid);
|
||||||
|
if (enrolledTids.size) tdict = await training.getList(Array.from(enrolledTids));
|
||||||
|
}
|
||||||
|
for (const tdoc in tdocs) tdict[tdoc._id] = tdoc;
|
||||||
|
this.response.template = 'training_main.html';
|
||||||
|
this.response.body = {
|
||||||
|
tdocs, page, tpcount, qs, tsdict, tdict,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrainingDetailHandler extends TrainingHandler {
|
||||||
|
async get({ tid }) {
|
||||||
|
const tdoc = await training.get(tid);
|
||||||
|
const pids = training.getPids(tdoc);
|
||||||
|
// TODO(twd2): check status, eg. test, hidden problem, ...
|
||||||
|
const f = this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN) ? {} : { hidden: false };
|
||||||
|
const [udoc, pdict] = await Promise.all([
|
||||||
|
user.getById(tdoc.owner),
|
||||||
|
problem.getList(pids, f),
|
||||||
|
]);
|
||||||
|
const psdict = await problem.getListStatus(this.user._id, Object.keys(pdict));
|
||||||
|
const donePids = new Set();
|
||||||
|
const progPids = new Set();
|
||||||
|
for (const pid in psdict) {
|
||||||
|
const psdoc = psdict[pid];
|
||||||
|
if (psdoc.status) {
|
||||||
|
if (psdoc.status === builtin.status.STATUS_ACCEPTED) donePids.add(pid);
|
||||||
|
else progPids.add(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nsdict = {};
|
||||||
|
const ndict = {};
|
||||||
|
const doneNids = new Set();
|
||||||
|
for (const node of tdoc.dag) {
|
||||||
|
ndict[node._id] = node;
|
||||||
|
const totalCount = node.pids.length;
|
||||||
|
const doneCount = Set.union(new Set(node.pids), new Set(donePids));
|
||||||
|
const nsdoc = {
|
||||||
|
progress: totalCount ? parseInt(100 * (doneCount / totalCount)) : 100,
|
||||||
|
isDone: training.isDone(node, doneNids, donePids),
|
||||||
|
isProgress: training.isProgress(node, doneNids, donePids, progPids),
|
||||||
|
isOpen: training.isOpen(node, doneNids, donePids, progPids),
|
||||||
|
isInvalid: training.isInvalid(node, doneNids),
|
||||||
|
};
|
||||||
|
if (nsdoc.isDone) doneNids.add(node._id);
|
||||||
|
nsdict[node._id] = nsdoc;
|
||||||
|
}
|
||||||
|
const tsdoc = await training.setStatus(tdoc._id, this.user._id, {
|
||||||
|
doneNids: Array.from(doneNids),
|
||||||
|
donePids: Array.from(donePids),
|
||||||
|
done: doneNids.size === tdoc.dag.length,
|
||||||
|
});
|
||||||
|
const path = [
|
||||||
|
['training_main', 'training_main'],
|
||||||
|
[tdoc.title, null, true],
|
||||||
|
];
|
||||||
|
this.response.template = 'training_detail.html';
|
||||||
|
this.response.body = {
|
||||||
|
path, tdoc, tsdoc, pids, pdict, psdict, ndict, nsdict, udoc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async postEnroll({ tid }) {
|
||||||
|
this.checkPerm(PERM_LOGGEDIN);
|
||||||
|
const tdoc = await training.get(tid);
|
||||||
|
await training.enroll(tdoc._id, this.user._id);
|
||||||
|
this.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrainingCreateHandler extends TrainingHandler {
|
||||||
|
async prepare() {
|
||||||
|
this.checkPerm(PERM_LOGGEDIN);
|
||||||
|
this.checkPerm(PERM_CREATE_TRAINING);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get() {
|
||||||
|
this.response.template = 'training_edit.html';
|
||||||
|
this.response.body = { page_name: 'training_create' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async post({
|
||||||
|
title, content, dag, description,
|
||||||
|
}) {
|
||||||
|
dag = await _parseDagJson(dag);
|
||||||
|
const pids = training.getPids({ dag });
|
||||||
|
console.log(pids);
|
||||||
|
console.log(pids.length, pids.size);
|
||||||
|
assert(pids.length, new ValidationError('dag'));
|
||||||
|
const pdocs = await problem.getMulti({
|
||||||
|
$or: [{ _id: { $in: pids } }, { pid: { $in: pids } }],
|
||||||
|
}).sort('_id', 1).toArray();
|
||||||
|
const existPids = pdocs.map((pdoc) => pdoc._id);
|
||||||
|
const existPnames = pdocs.map((pdoc) => pdoc.pid);
|
||||||
|
if (pids.length !== existPids.length) {
|
||||||
|
for (const pid in pids) {
|
||||||
|
assert(
|
||||||
|
existPids.includes(pid) || existPnames.includes(pid),
|
||||||
|
new ProblemNotFoundError(pid),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const pdoc in pdocs) {
|
||||||
|
if (pdoc.hidden) this.checkPerm(PERM_VIEW_PROBLEM_HIDDEN);
|
||||||
|
}
|
||||||
|
const tid = await training.add(title, content, this.user._id, dag, description);
|
||||||
|
this.response.body = { tid };
|
||||||
|
this.response.redirect = `/t/${tid}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrainingEditHandler extends TrainingHandler {
|
||||||
|
async get({ tid }) {
|
||||||
|
const tdoc = await training.get(tid);
|
||||||
|
if (tdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_TRAINING);
|
||||||
|
const dag = JSON.stringify(tdoc.dag, null, 2);
|
||||||
|
const path = [
|
||||||
|
['training_main', '/t'],
|
||||||
|
[tdoc.title, `/t/${tdoc._id}`, true],
|
||||||
|
['training_edit', null],
|
||||||
|
];
|
||||||
|
this.response.template = 'training_edit.html';
|
||||||
|
this.response.body = {
|
||||||
|
tdoc, dag, path, page_name: 'training_edit',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async post({
|
||||||
|
tid, title, content, dag, description,
|
||||||
|
}) {
|
||||||
|
const tdoc = await training.get(tid);
|
||||||
|
if (!this.user._id === tdoc.owner) this.checkPerm(PERM_EDIT_TRAINING);
|
||||||
|
dag = await _parseDagJson(dag);
|
||||||
|
const pids = training.getPids({ dag });
|
||||||
|
assert(pids.length, new ValidationError('dag'));
|
||||||
|
const pdocs = await problem.getMulti({
|
||||||
|
$or: [
|
||||||
|
{ _id: { $in: pids } },
|
||||||
|
{ pid: { $in: pids } },
|
||||||
|
],
|
||||||
|
}).sort('_id', 1).toArray();
|
||||||
|
const existPids = pdocs.map((pdoc) => pdoc._id);
|
||||||
|
const existPnames = pdocs.map((pdoc) => pdoc.pid);
|
||||||
|
if (pids.length !== existPids.length) {
|
||||||
|
for (const pid in pids) {
|
||||||
|
assert(
|
||||||
|
existPids.includes(pid) || existPnames.includes(pid),
|
||||||
|
new ProblemNotFoundError(pid),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const pdoc in pdocs) { if (pdoc.hidden) this.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); }
|
||||||
|
await training.edit(tid, {
|
||||||
|
title, content, dag, description,
|
||||||
|
});
|
||||||
|
this.response.body = { tid };
|
||||||
|
this.response.redirect = `/t/${tid}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Route('/t', TrainingMainHandler);
|
||||||
|
Route('/t/:tid', TrainingDetailHandler);
|
||||||
|
Route('/t/:tid/edit', TrainingEditHandler);
|
||||||
|
Route('/training/create', TrainingCreateHandler);
|
||||||
@@ -21,6 +21,10 @@ const isName = (s) => s && s.length < 256;
|
|||||||
const checkName = (s) => { if (!isName(s)) throw new ValidationError('name'); else return s; };
|
const checkName = (s) => { if (!isName(s)) throw new ValidationError('name'); else return s; };
|
||||||
const isPid = (s) => RE_PID.test(s.toString());
|
const isPid = (s) => RE_PID.test(s.toString());
|
||||||
const checkPid = (s) => { if (!RE_PID.test(s)) throw new ValidationError('pid'); else return s; };
|
const checkPid = (s) => { if (!RE_PID.test(s)) throw new ValidationError('pid'); else return s; };
|
||||||
|
const isIntro = () => true;
|
||||||
|
const checkIntro = (s) => { if (!isIntro(s)) throw new ValidationError('intro'); else return s; };
|
||||||
|
const isDescription = () => true;
|
||||||
|
const checkDescription = (s) => { if (!isDescription(s)) throw new ValidationError('description'); else return s; };
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
isTitle,
|
isTitle,
|
||||||
@@ -39,6 +43,10 @@ module.exports = {
|
|||||||
checkName,
|
checkName,
|
||||||
isPid,
|
isPid,
|
||||||
checkPid,
|
checkPid,
|
||||||
|
isIntro,
|
||||||
|
checkIntro,
|
||||||
|
isDescription,
|
||||||
|
checkDescription,
|
||||||
};
|
};
|
||||||
/*
|
/*
|
||||||
ID_RE = re.compile(r'[^\\/\s\u3000]([^\\/\n\r]*[^\\/\s\u3000])?')
|
ID_RE = re.compile(r'[^\\/\s\u3000]([^\\/\n\r]*[^\\/\s\u3000])?')
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ async def recalc_status(domainId: str, doc_type: int, cid: objeccid.Objeccid):
|
|||||||
tdoc = await document.get(domainId, doc_type, cid)
|
tdoc = await document.get(domainId, doc_type, cid)
|
||||||
async with document.get_multi_status(domainId=domainId,
|
async with document.get_multi_status(domainId=domainId,
|
||||||
doc_type=doc_type,
|
doc_type=doc_type,
|
||||||
doc_id=tdoc['doc_id']) as tsdocs:
|
doc_id=tdoc._id) as tsdocs:
|
||||||
async for tsdoc in tsdocs:
|
async for tsdoc in tsdocs:
|
||||||
if 'journal' not in tsdoc or not tsdoc['journal']:
|
if 'journal' not in tsdoc or not tsdoc['journal']:
|
||||||
continue
|
continue
|
||||||
|
|||||||
+16
-1
@@ -56,7 +56,7 @@ async function add({
|
|||||||
async function get(pid, uid = null) {
|
async function get(pid, uid = null) {
|
||||||
let query = {};
|
let query = {};
|
||||||
if (pid.generationTime || pid.length === 24) query = { _id: new ObjectID(pid) };
|
if (pid.generationTime || pid.length === 24) query = { _id: new ObjectID(pid) };
|
||||||
else query = { pid: parseInt(pid) || pid };
|
else query = { pid };
|
||||||
const pdoc = await coll.findOne(query);
|
const pdoc = await coll.findOne(query);
|
||||||
if (!pdoc) throw new ProblemNotFoundError(pid);
|
if (!pdoc) throw new ProblemNotFoundError(pid);
|
||||||
if (uid) {
|
if (uid) {
|
||||||
@@ -91,6 +91,13 @@ function getMany(query, sort, page, limit) {
|
|||||||
function getMulti(query) {
|
function getMulti(query) {
|
||||||
return coll.find(query);
|
return coll.find(query);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @param {object} query
|
||||||
|
* @returns {Cursor}
|
||||||
|
*/
|
||||||
|
function getMultiStatus(query) {
|
||||||
|
return collStatus.find(query);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* @param {ObjectID} _id
|
* @param {ObjectID} _id
|
||||||
* @param {object} query
|
* @param {object} query
|
||||||
@@ -120,6 +127,12 @@ async function getList(pids) {
|
|||||||
for (const pid of pids) r[pid] = await get(pid); // eslint-disable-line no-await-in-loop
|
for (const pid of pids) r[pid] = await get(pid); // eslint-disable-line no-await-in-loop
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
async function getListStatus(uid, pids) {
|
||||||
|
const psdocs = await getMultiStatus({ uid, pid: { $in: Array.from(new Set(pids)) } }).toArray();
|
||||||
|
const r = {};
|
||||||
|
for (const psdoc of psdocs) r[psdoc.pid] = psdoc;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
add,
|
add,
|
||||||
@@ -131,4 +144,6 @@ module.exports = {
|
|||||||
getById,
|
getById,
|
||||||
getMulti,
|
getMulti,
|
||||||
getList,
|
getList,
|
||||||
|
getListStatus,
|
||||||
|
getMultiStatus,
|
||||||
};
|
};
|
||||||
+74
-41
@@ -1,67 +1,100 @@
|
|||||||
|
const assert = require('assert');
|
||||||
const validator = require('../lib/validator');
|
const validator = require('../lib/validator');
|
||||||
const { ValidationError, TrainingNotFoundError } = require('../error');
|
const { ValidationError, TrainingNotFoundError, TrainingAlreadyEnrollError } = require('../error');
|
||||||
const db = require('../service/db.js');
|
const db = require('../service/db.js');
|
||||||
|
|
||||||
const coll = db.collection('traning');
|
const coll = db.collection('traning');
|
||||||
const collStatus = db.collection('training.status');
|
const collStatus = db.collection('training.status');
|
||||||
|
|
||||||
module.exports = {
|
async function enroll(tid, uid) {
|
||||||
SETTING_DIFFICULTY_ALGORITHM: 0,
|
try {
|
||||||
SETTING_DIFFICULTY_ADMIN: 1,
|
await collStatus.insertOne({ tid, uid, enroll: 1 });
|
||||||
SETTING_DIFFICULTY_AVERAGE: 2,
|
} catch (e) {
|
||||||
|
throw new TrainingAlreadyEnrollError(tid, uid);
|
||||||
|
}
|
||||||
|
await coll.findOneAndUpdate({ _id: tid }, { $inc: { enroll: 1 } });
|
||||||
|
}
|
||||||
|
async function setStatus(tid, uid, $set) {
|
||||||
|
await collStatus.findOneAndUpdate({ tid, uid }, { $set });
|
||||||
|
return await collStatus.findOne({ tid, uid });
|
||||||
|
}
|
||||||
|
|
||||||
async add(title, content, owner, dag = [], desc = '') {
|
module.exports = {
|
||||||
|
getPids(tdoc) {
|
||||||
|
console.log(tdoc.dag);
|
||||||
|
const pids = new Set();
|
||||||
|
for (const node of tdoc.dag) {
|
||||||
|
for (const pid of node.pids) pids.add(pid);
|
||||||
|
}
|
||||||
|
return Array.from(pids);
|
||||||
|
},
|
||||||
|
isDone(node, doneNids, donePids) {
|
||||||
|
return (Set.isSuperset(new Set(doneNids), new Set(node.requireNids))
|
||||||
|
&& Set.isSuperset(new Set(donePids), new Set(node.pids)));
|
||||||
|
},
|
||||||
|
isProgress(node, doneNids, donePids, progPids) {
|
||||||
|
return (Set.isSuperset(new Set(doneNids), new Set(node.requireNids))
|
||||||
|
&& !Set.isSuperset(new Set(donePids), new Set(node.pids))
|
||||||
|
&& Set.intersection(
|
||||||
|
Set.union(new Set(donePids), new Set(progPids)),
|
||||||
|
new Set(node.pids),
|
||||||
|
).size);
|
||||||
|
},
|
||||||
|
isOpen(node, doneNids, donePids, progPids) {
|
||||||
|
return (Set.isSuperset(new Set(doneNids), new Set(node.requireNids))
|
||||||
|
&& !Set.isSuperset(new Set(donePids), new Set(node.pids))
|
||||||
|
&& !Set.intersection(
|
||||||
|
Set.union(new Set(donePids), new Set(progPids)),
|
||||||
|
new Set(node.pids),
|
||||||
|
).size);
|
||||||
|
},
|
||||||
|
isInvalid(node, doneNids) {
|
||||||
|
return (!Set.isSuperset(new Set(doneNids), new Set(node.requireNids)));
|
||||||
|
},
|
||||||
|
async add(title, content, owner, dag = [], description = '') {
|
||||||
validator.checkTitle(title);
|
validator.checkTitle(title);
|
||||||
validator.checkIntro(content);
|
validator.checkIntro(content);
|
||||||
validator.checkDescription(desc);
|
validator.checkDescription(description);
|
||||||
for (const node of dag) { for (const nid of node.require_nids) if (nid >= node._id) throw new ValidationError('dag'); }
|
for (const node of dag) {
|
||||||
return await coll.insertOne({
|
for (const nid of node.requireNids) {
|
||||||
|
if (nid >= node._id) throw new ValidationError('dag');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await coll.insertOne({
|
||||||
content,
|
content,
|
||||||
owner,
|
owner,
|
||||||
dag,
|
dag,
|
||||||
title,
|
title,
|
||||||
desc,
|
description,
|
||||||
enroll: 0,
|
enroll: 0,
|
||||||
});
|
});
|
||||||
|
return res.insertedId;
|
||||||
},
|
},
|
||||||
count: (query) => coll.find(query).count(),
|
count: (query) => coll.find(query).count(),
|
||||||
async edit(tid, $set) {
|
async edit(tid, $set) {
|
||||||
if ($set.title) validator.check_title($set.title);
|
if ($set.title) validator.checkTitle($set.title);
|
||||||
if ($set.content) validator.check_intro($set.content);
|
if ($set.content) validator.checkIntro($set.content);
|
||||||
if ($set.desc) validator.check_description($set.desc);
|
if ($set.desc) validator.checkDescription($set.description);
|
||||||
if ($set.dag) { for (const node of $set.dag) for (const nid of node.require_nids) if (nid >= node._id) throw new ValidationError('dag'); }
|
if ($set.dag) {
|
||||||
await coll.findOneAndUpdate({ tid }, { $set });
|
for (const node of $set.dag) {
|
||||||
const tdoc = await coll.findOne({ tid });
|
for (const nid of node.requireNids) {
|
||||||
|
assert(nid >= node._id, new ValidationError('dag'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await coll.findOneAndUpdate({ _id: tid }, { $set });
|
||||||
|
const tdoc = await coll.findOne({ _id: tid });
|
||||||
if (!tdoc) throw new TrainingNotFoundError(tid);
|
if (!tdoc) throw new TrainingNotFoundError(tid);
|
||||||
return tdoc;
|
return tdoc;
|
||||||
},
|
},
|
||||||
async get(tid) {
|
async get(tid) {
|
||||||
const tdoc = await coll.findOne({ tid });
|
const tdoc = await coll.findOne({ _id: tid });
|
||||||
if (!tdoc) throw new TrainingNotFoundError(tid);
|
if (!tdoc) throw new TrainingNotFoundError(tid);
|
||||||
return tdoc;
|
return tdoc;
|
||||||
},
|
},
|
||||||
get_multi: (query) => coll.find(query),
|
getMulti: (query) => coll.find(query),
|
||||||
get_multi_status: (query) => collStatus.find(query),
|
getMultiStatus: (query) => collStatus.find(query),
|
||||||
get_status: (tid, uid) => collStatus.findOne({ tid, uid }),
|
getStatus: (tid, uid) => collStatus.findOne({ tid, uid }),
|
||||||
set_status: (tid, uid, $set) => collStatus.findOneAndUpdate({ tid, uid }, { $set }),
|
enroll,
|
||||||
|
setStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
async def get_dict_status(domainId, uid, tids, *, fields=None):
|
|
||||||
result = dict()
|
|
||||||
async for tsdoc in get_multi_status(domainId=domainId,
|
|
||||||
uid=uid,
|
|
||||||
doc_id={'$in': list(set(tids))},
|
|
||||||
fields=fields):
|
|
||||||
result[tsdoc['doc_id']] = tsdoc
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def get_dict(domainId, tids, *, fields=None):
|
|
||||||
result = dict()
|
|
||||||
async for tdoc in get_multi(domainId=domainId,
|
|
||||||
doc_id={'$in': list(set(tids))},
|
|
||||||
fields=fields):
|
|
||||||
result[tdoc['doc_id']] = tdoc
|
|
||||||
return result
|
|
||||||
*/
|
|
||||||
@@ -42,6 +42,7 @@ let options = {
|
|||||||
RECORD_PER_PAGE: 100,
|
RECORD_PER_PAGE: 100,
|
||||||
SOLUTION_PER_PAGE: 20,
|
SOLUTION_PER_PAGE: 20,
|
||||||
CONTEST_PER_PAGE: 20,
|
CONTEST_PER_PAGE: 20,
|
||||||
|
TRAINING_PER_PAGE: 10,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -259,9 +259,9 @@ function Route(route, RouteHandler) {
|
|||||||
if (h[method]) await h[method](args);
|
if (h[method]) await h[method](args);
|
||||||
|
|
||||||
if (method === 'post' && ctx.request.body.operation) {
|
if (method === 'post' && ctx.request.body.operation) {
|
||||||
if (h[`${method}_${ctx.request.body.operation}`]) {
|
const operation = `_${ctx.request.body.operation}`
|
||||||
await h[`${method}_${ctx.request.body.operation}`](args);
|
.replace(/_([a-z])/gm, (s) => s[1].toUpperCase());
|
||||||
}
|
if (h[`${method}${operation}`]) await h[`${method}${operation}`](args);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (h.cleanup) await h.cleanup(args);
|
if (h.cleanup) await h.cleanup(args);
|
||||||
|
|||||||
@@ -36,3 +36,22 @@ Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') {
|
|||||||
.replace('%M', this.getMinutes())
|
.replace('%M', this.getMinutes())
|
||||||
.replace('%S', this.getSeconds());
|
.replace('%S', this.getSeconds());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Set.isSuperset = function isSuperset(set, subset) {
|
||||||
|
for (const elem of subset) {
|
||||||
|
if (!set.has(elem)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
Set.union = function union(setA, setB) {
|
||||||
|
const _union = new Set(setA);
|
||||||
|
for (const elem of setB) _union.add(elem);
|
||||||
|
return _union;
|
||||||
|
};
|
||||||
|
Set.intersection = function intersection(setA, setB) {
|
||||||
|
const _intersection = new Set();
|
||||||
|
for (const elem of setB) {
|
||||||
|
if (setA.has(elem)) _intersection.add(elem);
|
||||||
|
}
|
||||||
|
return _intersection;
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
>
|
>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{% if pdoc['_id']|string|length < 10 %}P{{ pdoc['_id'] }} {% endif %}{{ pdoc['title'] }}
|
{{ pdoc['pid'] }} {{ pdoc['title'] }}
|
||||||
{%- if not invalid %}
|
{%- if not invalid %}
|
||||||
</a>
|
</a>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
label:'Title',
|
label:'Title',
|
||||||
name:'title',
|
name:'title',
|
||||||
placeholder:_('title'),
|
placeholder:_('title'),
|
||||||
value:tdoc['title']|default(''),
|
value:tdoc.title|default(''),
|
||||||
autofocus:true,
|
autofocus:true,
|
||||||
row:false
|
row:false
|
||||||
}) }}
|
}) }}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="status_title">{{ _(title) }}</h2>
|
<h2 class="status_title">{{ _(title) }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ tdoc['title'] }}</h1>
|
<h1>{{ tdoc.title }}</h1>
|
||||||
<ul class="info">
|
<ul class="info">
|
||||||
<li>
|
<li>
|
||||||
<span class="icon icon-award"></span>
|
<span class="icon icon-award"></span>
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="media__body medium">
|
<div class="media__body medium">
|
||||||
<h1 class="contest__title"><a href="/c/{{ tdoc._id }}" data-emoji-enabled>{{ tdoc['title'] }}</a></h1>
|
<h1 class="contest__title"><a href="/c/{{ tdoc._id }}" data-emoji-enabled>{{ tdoc.title }}</a></h1>
|
||||||
<ul class="supplementary list">
|
<ul class="supplementary list">
|
||||||
<li>
|
<li>
|
||||||
<a href="?rule={{ tdoc['rule'] }}" class="contest-type-tag"><span class="icon icon-award"></span>{{ model.contest.RULES[tdoc.rule].TEXT }}</a>
|
<a href="?rule={{ tdoc['rule'] }}" class="contest-type-tag"><span class="icon icon-award"></span>{{ model.contest.RULES[tdoc.rule].TEXT }}</a>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="medium-3 columns">
|
<div class="medium-3 columns">
|
||||||
{% with udoc = udict[ddoc['owner']] %}
|
{% set udoc = udict[ddoc['owner']] %}
|
||||||
<div class="section side">
|
<div class="section side">
|
||||||
<div class="profile__bg user-profile-bg--{{ vj4.model.adaptor.setting.UserSetting(udoc).get_setting('background_img') }}"></div>
|
<div class="profile__bg user-profile-bg--{{ vj4.model.adaptor.setting.UserSetting(udoc).get_setting('background_img') }}"></div>
|
||||||
<div class="section__body">
|
<div class="section__body">
|
||||||
@@ -119,13 +119,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
|
||||||
</div>
|
</div>
|
||||||
{% if vnode['doc_type'] == vj4.model.document.TYPE_PROBLEM %}
|
{% set pdoc=vnode %}
|
||||||
{% with pdoc=vnode, owner_udoc=udict[vnode['owner']] %}
|
{% set owner_udoc=udict[vnode['owner']] %}
|
||||||
{% include "partials/problem_sidebar.html" %}
|
{% include "partials/problem_sidebar.html" %}
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="media__body medium">
|
<div class="media__body medium">
|
||||||
<h1 class="contest__title"><a href="/c/{{ tdoc._id }}" data-emoji-enabled>{{ tdoc['title'] }}</a></h1>
|
<h1 class="contest__title"><a href="/c/{{ tdoc._id }}" data-emoji-enabled>{{ tdoc.title }}</a></h1>
|
||||||
<ul class="supplementary list">
|
<ul class="supplementary list">
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ reverse_url('contest_main') }}?rule={{ tdoc['rule'] }}" class="contest-type-tag"><span class="icon icon-award"></span>{{ vj4.constant.contest.RULE_TEXTS[tdoc['rule']] }}</a>
|
<a href="{{ reverse_url('contest_main') }}?rule={{ tdoc['rule'] }}" class="contest-type-tag"><span class="icon icon-award"></span>{{ vj4.constant.contest.RULE_TEXTS[tdoc['rule']] }}</a>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<span class="icon icon-user--multiple"></span> {{ tdoc['attend']|default(0) }}
|
<span class="icon icon-user--multiple"></span> {{ tdoc['attend']|default(0) }}
|
||||||
</li>
|
</li>
|
||||||
{% if tsdict[tdoc['doc_id']]['attend'] == 1 %}
|
{% if tsdict[tdoc._id]['attend'] == 1 %}
|
||||||
<li class="contest__info-attended">
|
<li class="contest__info-attended">
|
||||||
<span class="icon icon-check"></span> {{ _('Attended') }}
|
<span class="icon icon-check"></span> {{ _('Attended') }}
|
||||||
</li>
|
</li>
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="media__body middle">
|
<div class="media__body middle">
|
||||||
<h1 class="training__title"><a href="{{ reverse_url('training_detail', tid=tdoc['doc_id']) }}" data-emoji-enabled>{{ tdoc['title'] }}</a></h1>
|
<h1 class="training__title"><a href="{{ reverse_url('training_detail', tid=tdoc._id) }}" data-emoji-enabled>{{ tdoc.title }}</a></h1>
|
||||||
<div class="training__intro typo">
|
<div class="training__intro typo">
|
||||||
<p>{{ tdoc['content'] }}</p>
|
<p>{{ tdoc['content'] }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,10 +85,10 @@
|
|||||||
<span class="icon icon-flag text-blue"></span> {{ _('{0} sections').format(tdoc['dag']|length) }}, {{ _('{0} problems').format(training.get_pids(tdoc)|length) }}
|
<span class="icon icon-flag text-blue"></span> {{ _('{0} sections').format(tdoc['dag']|length) }}, {{ _('{0} problems').format(training.get_pids(tdoc)|length) }}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{% if tsdict[tdoc['doc_id']]['enroll'] %}
|
{% if tsdict[tdoc._id]['enroll'] %}
|
||||||
{% if not tsdict[tdoc['doc_id']]['done'] %}
|
{% if not tsdict[tdoc._id]['done'] %}
|
||||||
<span class="icon training-status--icon progress"></span>
|
<span class="icon training-status--icon progress"></span>
|
||||||
<span class="training-status--text progress">{{ _('Completed') }} {{ (100 * tsdict[tdoc['doc_id']]['done_pids']|length / training.get_pids(tdoc)|length)|round|int }}%</span>
|
<span class="training-status--text progress">{{ _('Completed') }} {{ (100 * tsdict[tdoc._id]['done_pids']|length / training.get_pids(tdoc)|length)|round|int }}%</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="icon training-status--icon done"></span>
|
<span class="icon training-status--icon done"></span>
|
||||||
<span class="training-status--text done">{{ _('Completed') }} 100%</span>
|
<span class="training-status--text done">{{ _('Completed') }} 100%</span>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{% if page_name != 'contest_detail' %}
|
{% if page_name != 'contest_detail' %}
|
||||||
<a class="contest-sidebar__bg" href="/c/{{ tdoc._id }}">
|
<a class="contest-sidebar__bg" href="/c/{{ tdoc._id }}">
|
||||||
<div class="section__body">
|
<div class="section__body">
|
||||||
<h1>{{ tdoc['title'] }}</h1>
|
<h1>{{ tdoc.title }}</h1>
|
||||||
<div class="contest-sidebar__status">
|
<div class="contest-sidebar__status">
|
||||||
{% if attended %}
|
{% if attended %}
|
||||||
<span class="icon icon-check"></span> {{ _('Attended') }}
|
<span class="icon icon-check"></span> {{ _('Attended') }}
|
||||||
|
|||||||
@@ -168,13 +168,13 @@
|
|||||||
{% if tdocs %}
|
{% if tdocs %}
|
||||||
<p>{{ _('In following training plans') }}: </p>
|
<p>{{ _('In following training plans') }}: </p>
|
||||||
{% for tdoc in tdocs %}
|
{% for tdoc in tdocs %}
|
||||||
<p><a href="{{ reverse_url('training_detail', tid=tdoc['doc_id']) }}">{{ tdoc['title'] }}</a></p>
|
<p><a href="{{ reverse_url('training_detail', tid=tdoc._id) }}">{{ tdoc.title }}</a></p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if ctdocs %}
|
{% if ctdocs %}
|
||||||
<p>{{ _('In following contests') }}: </p>
|
<p>{{ _('In following contests') }}: </p>
|
||||||
{% for tdoc in ctdocs %}
|
{% for tdoc in ctdocs %}
|
||||||
<p><a href="/c/{{ tdoc._id }}">{{ tdoc['title'] }}</a></p>
|
<p><a href="/c/{{ tdoc._id }}">{{ tdoc.title }}</a></p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
{% if tdoc %}
|
{% if tdoc %}
|
||||||
<dt>{{ _('Contest') }}</dt>
|
<dt>{{ _('Contest') }}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<a href="/c/{{ tdoc._id }}">{{ tdoc['title'] }}</a>
|
<a href="/c/{{ tdoc._id }}">{{ tdoc.title }}</a>
|
||||||
</dd>
|
</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if pdoc and (user._id == pdoc.owner or handler.hasPerm(perm.PERM_READ_PROBLEM_DATA)) %}
|
{% if pdoc and (user._id == pdoc.owner or handler.hasPerm(perm.PERM_READ_PROBLEM_DATA)) %}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{% set page_name = "training_detail" %}
|
||||||
{% import "components/record.html" as record with context %}
|
{% import "components/record.html" as record with context %}
|
||||||
{% import "components/problem.html" as problem with context %}
|
{% import "components/problem.html" as problem with context %}
|
||||||
{% extends "layout/basic.html" %}
|
{% extends "layout/basic.html" %}
|
||||||
@@ -13,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section">
|
<div class="section">
|
||||||
{% if not tsdoc['enroll'] %}
|
{% if not tsdoc.enroll %}
|
||||||
<div class="section__body">
|
<div class="section__body">
|
||||||
<blockquote class="typo warn">
|
<blockquote class="typo warn">
|
||||||
<p>{{ _('page.training_detail.invalid_when_not_enrolled') }}</p>
|
<p>{{ _('page.training_detail.invalid_when_not_enrolled') }}</p>
|
||||||
@@ -21,19 +22,19 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for node in tdoc['dag'] %}
|
{% for node in tdoc['dag'] %}
|
||||||
<div class="training__section {% if nsdict[node['_id']]['isDone'] %}done{% elif nsdict[node['_id']]['is_progress'] %}progress{% elif nsdict[node['_id']]['is_open'] %}open{% else %}invalid{% endif %} {% if nsdict[node['_id']]['is_progress'] or nsdict[node['_id']]['is_open'] %}expanded{% else %}collapsed{% endif %}">
|
<div class="training__section {% if nsdict[node['_id']]['isDone'] %}done{% elif nsdict[node['_id']]['isProgress'] %}progress{% elif nsdict[node['_id']]['isOpen'] %}open{% else %}invalid{% endif %} {% if nsdict[node['_id']]['isProgress'] or nsdict[node['_id']]['isOpen'] %}expanded{% else %}collapsed{% endif %}">
|
||||||
<div class="section__header clearfix">
|
<div class="section__header clearfix">
|
||||||
<div class="float-left">
|
<div class="float-left">
|
||||||
<h1 class="section__title">{{ _('Section') }} {{ node['_id'] }}. {{ node['title'] }}</h1>
|
<h1 class="section__title">{{ _('Section') }} {{ node['_id'] }}. {{ node['title'] }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="float-right">
|
<div class="float-right">
|
||||||
<h1 class="section__title training-section-status--text {% if nsdict[node['_id']]['isDone'] %}done{% elif nsdict[node['_id']]['is_progress'] %}progress{% elif nsdict[node['_id']]['is_open'] %}open{% else %}invalid{% endif %}">
|
<h1 class="section__title training-section-status--text {% if nsdict[node['_id']]['isDone'] %}done{% elif nsdict[node['_id']]['isProgress'] %}progress{% elif nsdict[node['_id']]['isOpen'] %}open{% else %}invalid{% endif %}">
|
||||||
<span class="icon training-section-status--icon {% if nsdict[node['_id']]['isDone'] %}done{% elif nsdict[node['_id']]['is_progress'] %}progress{% elif nsdict[node['_id']]['is_open'] %}open{% else %}invalid{% endif %}"></span>
|
<span class="icon training-section-status--icon {% if nsdict[node['_id']]['isDone'] %}done{% elif nsdict[node['_id']]['isProgress'] %}progress{% elif nsdict[node['_id']]['isOpen'] %}open{% else %}invalid{% endif %}"></span>
|
||||||
{% if nsdict[node['_id']]['isDone'] %}
|
{% if nsdict[node['_id']]['isDone'] %}
|
||||||
{{ _('Completed') }}
|
{{ _('Completed') }}
|
||||||
{% elif nsdict[node['_id']]['is_progress'] %}
|
{% elif nsdict[node['_id']]['isProgress'] %}
|
||||||
{{ _('In Progress') }}
|
{{ _('In Progress') }}
|
||||||
{% elif nsdict[node['_id']]['is_open'] %}
|
{% elif nsdict[node['_id']]['isOpen'] %}
|
||||||
{{ _('Open') }}
|
{{ _('Open') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ _('Invalid') }}
|
{{ _('Invalid') }}
|
||||||
@@ -59,7 +60,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="training__section__detail">
|
<div class="training__section__detail">
|
||||||
{% if nsdict[node['_id']]['is_invalid'] %}
|
{% if nsdict[node['_id']]['isInvalid'] %}
|
||||||
<div class="section__body">
|
<div class="section__body">
|
||||||
<blockquote class="typo note">
|
<blockquote class="typo note">
|
||||||
<p>{{ _('This section cannot be challenged at present, so please complete the following sections first') }}:</p>
|
<p>{{ _('This section cannot be challenged at present, so please complete the following sections first') }}:</p>
|
||||||
@@ -73,7 +74,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if node['content'] %}
|
{% if node['content'] %}
|
||||||
<div class="section__body typo">
|
<div class="section__body typo">
|
||||||
{{ node['content']|markdown }}
|
{{ node['content']|markdown|safe }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="section__body no-padding training__problems">
|
<div class="section__body no-padding training__problems">
|
||||||
@@ -101,7 +102,8 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for pid in node['pids'] %}
|
{% for pid in node['pids'] %}
|
||||||
{% if pid in pdict %}
|
{% if pid in pdict %}
|
||||||
{% with pdoc=pdict[pid], psdoc=psdict[pid] %}
|
{% set pdoc=pdict[pid] %}
|
||||||
|
{% set psdoc=psdict[pid] %}
|
||||||
<tr>
|
<tr>
|
||||||
{% if handler.hasPerm(perm.PERM_LOGGEDIN) %}
|
{% if handler.hasPerm(perm.PERM_LOGGEDIN) %}
|
||||||
{% if psdoc['rid'] %}
|
{% if psdoc['rid'] %}
|
||||||
@@ -125,17 +127,15 @@
|
|||||||
{{ problem.render_problem_title(
|
{{ problem.render_problem_title(
|
||||||
pdoc,
|
pdoc,
|
||||||
invalid=not tsdoc['enroll'],
|
invalid=not tsdoc['enroll'],
|
||||||
show_tags=false,
|
show_tags=false
|
||||||
rp=vj4.job.rp.get_rp_expect(pdoc) if (not psdoc or psdoc['status'] != status.STATUS_ACCEPTED) else none
|
|
||||||
) }}
|
) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="col--submit_n">{{ pdoc.nSubmit }}</td>
|
<td class="col--submit_n">{{ pdoc.nSubmit }}</td>
|
||||||
<td class="col--ac_rate">{{ (100 * pdoc.nAccept / pdoc.nSubmit)|round|int if pdoc.nSubmit > 0 else _('?') }}</td>
|
<td class="col--ac_rate">{{ (100 * pdoc.nAccept / pdoc.nSubmit)|round|int if pdoc.nSubmit > 0 else _('?') }}</td>
|
||||||
<td class="col--difficulty">{{ pdoc['difficulty'] or _('(None)') }}</td>
|
<td class="col--difficulty">{{ pdoc['difficulty'] or _('(None)') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endwith %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% with pdoc = {'domain_id': handler.domain_id, 'doc_id': pid, 'hidden': true, 'title': '*'} %}
|
{% set pdoc = {'_id': pid, 'hidden': true, 'title': '*'} %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="col--status record-status--border">
|
<td class="col--status record-status--border">
|
||||||
</td>
|
</td>
|
||||||
@@ -146,7 +146,6 @@
|
|||||||
<td class="col--ac_rate">*</td>
|
<td class="col--ac_rate">*</td>
|
||||||
<td class="col--difficulty">*</td>
|
<td class="col--difficulty">*</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -175,8 +174,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if handler.own(tdoc, perm.PERM_EDIT_TRAINING_SELF) or handler.hasPerm(perm.PERM_EDIT_TRAINING) %}
|
{% if tdoc.owner == user._id or handler.hasPerm(perm.PERM_EDIT_TRAINING) %}
|
||||||
<li class="menu__item"><a class="menu__link" href="{{ reverse_url('training_edit', tid=tdoc['doc_id']) }}">
|
<li class="menu__item"><a class="menu__link" href="/t/{{ tdoc._id }}/edit">
|
||||||
<span class="icon icon-edit"></span> {{ _('Edit') }}
|
<span class="icon icon-edit"></span> {{ _('Edit') }}
|
||||||
</a></li>
|
</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -192,7 +191,7 @@
|
|||||||
<dt>{{ _('Status') }}</dt><dd>{% if tsdoc['enroll'] %}{{ _('Completed' if tsdoc['done'] else 'In Progress') }}{% else %}{{ _('Not Enrolled') }}{% endif %}</dd>
|
<dt>{{ _('Status') }}</dt><dd>{% if tsdoc['enroll'] %}{{ _('Completed' if tsdoc['done'] else 'In Progress') }}{% else %}{{ _('Not Enrolled') }}{% endif %}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if tsdoc['enroll'] %}
|
{% if tsdoc['enroll'] %}
|
||||||
<dt>{{ _('Progress') }}</dt><dd>{{ _('Completed') }} {{ (100 * tsdoc['done_pids']|length / pids|length)|round|int }}%</dd>
|
<dt>{{ _('Progress') }}</dt><dd>{{ _('Completed') }} {{ (100 * tsdoc['donePids']|length / pids|length)|round|int }}%</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<dt>{{ _('Enrollees') }}</dt><dd>{{ tdoc['enroll']|default(0) }}</dd>
|
<dt>{{ _('Enrollees') }}</dt><dd>{{ tdoc['enroll']|default(0) }}</dd>
|
||||||
<dt>{{ _('Created By') }}</dt>
|
<dt>{{ _('Created By') }}</dt>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="medium-5 columns">
|
<div class="medium-5 columns">
|
||||||
<label>
|
<label>
|
||||||
{{ _('Title') }}
|
{{ _('Title') }}
|
||||||
<input name="title" placeholder="{{ _('title') }}" value="{{ tdoc['title']|default('') }}" class="textbox" autofocus>
|
<input name="title" placeholder="{{ _('title') }}" value="{{ tdoc.title|default('') }}" class="textbox" autofocus>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{% set page_name = "training_main" %}
|
||||||
{% extends "layout/basic.html" %}
|
{% extends "layout/basic.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -6,7 +7,7 @@
|
|||||||
<div class="section__header">
|
<div class="section__header">
|
||||||
<h1 class="section__title">{{ _('All Training Plans') }}</h1>
|
<h1 class="section__title">{{ _('All Training Plans') }}</h1>
|
||||||
</div>
|
</div>
|
||||||
{% if not tdocs %}
|
{% if not tdocs.length %}
|
||||||
{{ nothing.render('Sorry, there is no training plan.') }}
|
{{ nothing.render('Sorry, there is no training plan.') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<ol class="section__list all primary training__list">
|
<ol class="section__list all primary training__list">
|
||||||
@@ -15,24 +16,24 @@
|
|||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media__left middle">
|
<div class="media__left middle">
|
||||||
<div class="training__participants numbox">
|
<div class="training__participants numbox">
|
||||||
<div class="numbox__num large">{{ tdoc['enroll']|default(0) }}</div>
|
<div class="numbox__num large">{{ tdoc.enroll|default(0) }}</div>
|
||||||
<div class="numbox__text">{{ _('Enrolled') }}</div>
|
<div class="numbox__text">{{ _('Enrolled') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="media__body middle">
|
<div class="media__body middle">
|
||||||
<h1 class="training__title"><a href="{{ reverse_url('training_detail', tid=tdoc['doc_id']) }}" data-emoji-enabled>{{ tdoc['title'] }}</a></h1>
|
<h1 class="training__title"><a href="/t/{{ tdoc._id }}" data-emoji-enabled>{{ tdoc.title }}</a></h1>
|
||||||
<div class="training__intro typo">
|
<div class="training__intro typo">
|
||||||
<p>{{ tdoc['content'] }}</p>
|
<p>{{ tdoc['content'] }}</p>
|
||||||
</div>
|
</div>
|
||||||
<ul class="supplementary list training__progress">
|
<ul class="supplementary list training__progress">
|
||||||
<li>
|
<li>
|
||||||
<span class="icon icon-flag text-blue"></span> {{ _('{0} sections').format(tdoc['dag']|length) }}, {{ _('{0} problems').format(handler.get_pids(tdoc)|length) }}
|
<span class="icon icon-flag text-blue"></span> {{ _('{0} sections').format(tdoc['dag']|length) }}, {{ _('{0} problems').format(model.training.getPids(tdoc)|length) }}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{% if tsdict[tdoc['doc_id']]['enroll'] %}
|
{% if tsdict[tdoc._id]['enroll'] %}
|
||||||
{% if not tsdict[tdoc['doc_id']]['done'] %}
|
{% if not tsdict[tdoc._id]['done'] %}
|
||||||
<span class="icon training-status--icon progress"></span>
|
<span class="icon training-status--icon progress"></span>
|
||||||
<span class="training-status--text progress">{{ _('Completed') }} {{ (100 * tsdict[tdoc['doc_id']]['done_pids']|length / handler.get_pids(tdoc)|length)|round|int }}%</span>
|
<span class="training-status--text progress">{{ _('Completed') }} {{ (100 * tsdict[tdoc._id]['done_pids']|length / model.training.getPids(tdoc)|length)|round|int }}%</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="icon training-status--icon done"></span>
|
<span class="icon training-status--icon done"></span>
|
||||||
<span class="training-status--text done">{{ _('Completed') }} 100%</span>
|
<span class="training-status--text done">{{ _('Completed') }} 100%</span>
|
||||||
@@ -59,15 +60,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="section__body">
|
<div class="section__body">
|
||||||
<ol class="my secondary training__list">
|
<ol class="my secondary training__list">
|
||||||
{% for tsdoc in tsdict.values() %}
|
{% for tsdoc in tsdict %}
|
||||||
{% if tsdoc['enroll'] %}
|
{% if tsdoc['enroll'] %}
|
||||||
<li class="training__item"><div class="media">
|
<li class="training__item"><div class="media">
|
||||||
<div class="media__left">
|
<div class="media__left">
|
||||||
<span class="icon training-status--icon {% if tsdoc['done'] %}done{% else %}progress{% endif %}"></span>
|
<span class="icon training-status--icon {% if tsdoc['done'] %}done{% else %}progress{% endif %}"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="media__body">
|
<div class="media__body">
|
||||||
<h1 class="training__title"><a href="{{ reverse_url('training_detail', tid=tsdoc['doc_id']) }}" data-emoji-enabled>{{ tdict[tsdoc['doc_id']]['title'] }}</a></h1>
|
<h1 class="training__title"><a href="/t/{{ tsdoc.tid }}" data-emoji-enabled>{{ tdict[tsdoc['tid']]['title'] }}</a></h1>
|
||||||
<div class="supplementary training__progress"><div class="training__progress-bar"><div class="training__progress-track" style="width:{{ (100 * tsdoc['done_pids']|length / handler.get_pids(tdict[tsdoc['doc_id']])|length)|round|int }}%;"></div></div> {{ _('Complete') }} {{ (100 * tsdoc['done_pids']|length / handler.get_pids(tdict[tsdoc['doc_id']])|length)|round|int }}%</div>
|
<div class="supplementary training__progress"><div class="training__progress-bar"><div class="training__progress-track" style="width:{{ (100 * tsdoc['donePids']|length / model.training.getPids(tdict[tsdoc['tid']])|length)|round|int }}%;"></div></div> {{ _('Complete') }} {{ (100 * tsdoc['done_pids']|length / handler.get_pids(tdict[tsdoc['doc_id']])|length)|round|int }}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div></li>
|
</div></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -83,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ol class="menu">
|
<ol class="menu">
|
||||||
<li class="menu__item">
|
<li class="menu__item">
|
||||||
<a href="{{ reverse_url('training_create') }}" class="menu__link"><span class="icon icon-add"></span> {{ _('New Training Plan') }}</a>
|
<a href="/training/create" class="menu__link"><span class="icon icon-add"></span> {{ _('New Training Plan') }}</a>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<div class="section__body">
|
<div class="section__body">
|
||||||
|
|||||||
Reference in new issue
Block a user