diff --git a/README.md b/README.md index 1851c4ed..51453199 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,49 @@ Hydro是一个高效的信息学在线测评系统。 特点: 易于部署,轻量,功能强大。 +Hydro 的界面基于 Vijos 二次开发。 + ## 使用方式 -使用 `docker-compose`(推荐) +#### 使用 `docker-compose`(推荐) // 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/) 项目主要开发人员。 diff --git a/hydro/development.js b/hydro/development.js index d5ae617f..2dc7aa87 100644 --- a/hydro/development.js +++ b/hydro/development.js @@ -34,6 +34,7 @@ async function run() { require('./handler/judge'); require('./handler/user'); require('./handler/contest'); + require('./handler/training'); server.start(); } process.on('restart', async () => { diff --git a/hydro/error.js b/hydro/error.js index 4d9ddf05..8d8ee1dd 100644 --- a/hydro/error.js +++ b/hydro/error.js @@ -118,6 +118,12 @@ class ContestScoreboardHiddenError extends ForbiddenError { 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 { constructor(pid) { super('ProblemNotFoundError'); @@ -180,6 +186,7 @@ module.exports = { ContestAlreadyAttendedError, UserFacingError, SystemError, + TrainingAlreadyEnrollError, }; /* @@ -337,12 +344,6 @@ class TrainingRequirementNotSatisfiedError(ForbiddenError): return 'Training requirement is not satisfied.' -class TrainingAlreadyEnrollError(ForbiddenError): - @property - def message(self): - return "You've already enrolled this training." - - class UsageExceededError(ForbiddenError): @property def message(self): diff --git a/hydro/handler/problem.js b/hydro/handler/problem.js index b2de66f2..bdb33361 100644 --- a/hydro/handler/problem.js +++ b/hydro/handler/problem.js @@ -300,7 +300,7 @@ class ProblemSolutionReplyRawHandler extends ProblemDetailHandler { async get({ psid }) { this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION); 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.body = psrdoc.content; } diff --git a/hydro/handler/record.js b/hydro/handler/record.js index ca8b1c63..e10e9192 100644 --- a/hydro/handler/record.js +++ b/hydro/handler/record.js @@ -51,7 +51,9 @@ class RecordDetailHandler extends Handler { this.response.template = 'record_detail.html'; const rdoc = await record.get(rid); 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 = { path: [ ['Hydro', '/'], diff --git a/hydro/handler/training.js b/hydro/handler/training.js new file mode 100644 index 00000000..f21e8101 --- /dev/null +++ b/hydro/handler/training.js @@ -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); diff --git a/hydro/lib/validator.js b/hydro/lib/validator.js index dea4eabd..83c0c8c2 100644 --- a/hydro/lib/validator.js +++ b/hydro/lib/validator.js @@ -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 isPid = (s) => RE_PID.test(s.toString()); 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 = { isTitle, @@ -39,6 +43,10 @@ module.exports = { checkName, isPid, checkPid, + isIntro, + checkIntro, + isDescription, + checkDescription, }; /* ID_RE = re.compile(r'[^\\/\s\u3000]([^\\/\n\r]*[^\\/\s\u3000])?') diff --git a/hydro/model/contest.js b/hydro/model/contest.js index 565ded9a..6fe7daef 100644 --- a/hydro/model/contest.js +++ b/hydro/model/contest.js @@ -211,7 +211,7 @@ async def recalc_status(domainId: str, doc_type: int, cid: objeccid.Objeccid): tdoc = await document.get(domainId, doc_type, cid) async with document.get_multi_status(domainId=domainId, doc_type=doc_type, - doc_id=tdoc['doc_id']) as tsdocs: + doc_id=tdoc._id) as tsdocs: async for tsdoc in tsdocs: if 'journal' not in tsdoc or not tsdoc['journal']: continue diff --git a/hydro/model/problem.js b/hydro/model/problem.js index 3d04da5c..cab26d3d 100644 --- a/hydro/model/problem.js +++ b/hydro/model/problem.js @@ -56,7 +56,7 @@ async function add({ async function get(pid, uid = null) { let query = {}; 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); if (!pdoc) throw new ProblemNotFoundError(pid); if (uid) { @@ -91,6 +91,13 @@ function getMany(query, sort, page, limit) { function getMulti(query) { return coll.find(query); } +/** + * @param {object} query + * @returns {Cursor} + */ +function getMultiStatus(query) { + return collStatus.find(query); +} /** * @param {ObjectID} _id * @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 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 = { add, @@ -131,4 +144,6 @@ module.exports = { getById, getMulti, getList, + getListStatus, + getMultiStatus, }; diff --git a/hydro/model/training.js b/hydro/model/training.js index 5352dc79..12fcca79 100644 --- a/hydro/model/training.js +++ b/hydro/model/training.js @@ -1,67 +1,100 @@ +const assert = require('assert'); const validator = require('../lib/validator'); -const { ValidationError, TrainingNotFoundError } = require('../error'); +const { ValidationError, TrainingNotFoundError, TrainingAlreadyEnrollError } = require('../error'); const db = require('../service/db.js'); const coll = db.collection('traning'); const collStatus = db.collection('training.status'); -module.exports = { - SETTING_DIFFICULTY_ALGORITHM: 0, - SETTING_DIFFICULTY_ADMIN: 1, - SETTING_DIFFICULTY_AVERAGE: 2, +async function enroll(tid, uid) { + try { + await collStatus.insertOne({ tid, uid, enroll: 1 }); + } 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.checkIntro(content); - validator.checkDescription(desc); - for (const node of dag) { for (const nid of node.require_nids) if (nid >= node._id) throw new ValidationError('dag'); } - return await coll.insertOne({ + validator.checkDescription(description); + for (const node of dag) { + for (const nid of node.requireNids) { + if (nid >= node._id) throw new ValidationError('dag'); + } + } + const res = await coll.insertOne({ content, owner, dag, title, - desc, + description, enroll: 0, }); + return res.insertedId; }, count: (query) => coll.find(query).count(), async edit(tid, $set) { - if ($set.title) validator.check_title($set.title); - if ($set.content) validator.check_intro($set.content); - if ($set.desc) validator.check_description($set.desc); - if ($set.dag) { for (const node of $set.dag) for (const nid of node.require_nids) if (nid >= node._id) throw new ValidationError('dag'); } - await coll.findOneAndUpdate({ tid }, { $set }); - const tdoc = await coll.findOne({ tid }); + if ($set.title) validator.checkTitle($set.title); + if ($set.content) validator.checkIntro($set.content); + if ($set.desc) validator.checkDescription($set.description); + if ($set.dag) { + for (const node of $set.dag) { + 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); return tdoc; }, async get(tid) { - const tdoc = await coll.findOne({ tid }); + const tdoc = await coll.findOne({ _id: tid }); if (!tdoc) throw new TrainingNotFoundError(tid); return tdoc; }, - get_multi: (query) => coll.find(query), - get_multi_status: (query) => collStatus.find(query), - get_status: (tid, uid) => collStatus.findOne({ tid, uid }), - set_status: (tid, uid, $set) => collStatus.findOneAndUpdate({ tid, uid }, { $set }), - + getMulti: (query) => coll.find(query), + getMultiStatus: (query) => collStatus.find(query), + getStatus: (tid, uid) => collStatus.findOne({ tid, uid }), + 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 -*/ diff --git a/hydro/options.js b/hydro/options.js index 9685aec4..f05169e6 100644 --- a/hydro/options.js +++ b/hydro/options.js @@ -42,6 +42,7 @@ let options = { RECORD_PER_PAGE: 100, SOLUTION_PER_PAGE: 20, CONTEST_PER_PAGE: 20, + TRAINING_PER_PAGE: 10, }, }; diff --git a/hydro/service/server.js b/hydro/service/server.js index ea6a97ba..bc33d418 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -259,9 +259,9 @@ function Route(route, RouteHandler) { if (h[method]) await h[method](args); if (method === 'post' && ctx.request.body.operation) { - if (h[`${method}_${ctx.request.body.operation}`]) { - await h[`${method}_${ctx.request.body.operation}`](args); - } + const operation = `_${ctx.request.body.operation}` + .replace(/_([a-z])/gm, (s) => s[1].toUpperCase()); + if (h[`${method}${operation}`]) await h[`${method}${operation}`](args); } if (h.cleanup) await h.cleanup(args); diff --git a/hydro/utils.js b/hydro/utils.js index 03edb2b3..b6f20bc1 100644 --- a/hydro/utils.js +++ b/hydro/utils.js @@ -36,3 +36,22 @@ Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') { .replace('%M', this.getMinutes()) .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; +}; diff --git a/templates/components/problem.html b/templates/components/problem.html index 4d8a8f46..2954d33e 100644 --- a/templates/components/problem.html +++ b/templates/components/problem.html @@ -8,7 +8,7 @@ {%- endif %} > {%- endif %} - {% if pdoc['_id']|string|length < 10 %}P{{ pdoc['_id'] }} {% endif %}{{ pdoc['title'] }} + {{ pdoc['pid'] }} {{ pdoc['title'] }} {%- if not invalid %} {%- endif %} diff --git a/templates/contest_edit.html b/templates/contest_edit.html index 37ce5438..86fb1754 100644 --- a/templates/contest_edit.html +++ b/templates/contest_edit.html @@ -19,7 +19,7 @@ label:'Title', name:'title', placeholder:_('title'), - value:tdoc['title']|default(''), + value:tdoc.title|default(''), autofocus:true, row:false }) }} diff --git a/templates/contest_main.html b/templates/contest_main.html index 863f97c3..010f309a 100644 --- a/templates/contest_main.html +++ b/templates/contest_main.html @@ -9,7 +9,7 @@

{{ _(title) }}

-

{{ tdoc['title'] }}

+

{{ tdoc.title }}

- {% if nsdict[node['_id']]['is_invalid'] %} + {% if nsdict[node['_id']]['isInvalid'] %}

{{ _('This section cannot be challenged at present, so please complete the following sections first') }}:

@@ -73,7 +74,7 @@ {% endif %} {% if node['content'] %}
- {{ node['content']|markdown }} + {{ node['content']|markdown|safe }}
{% endif %}
@@ -101,7 +102,8 @@ {% for pid in node['pids'] %} {% if pid in pdict %} - {% with pdoc=pdict[pid], psdoc=psdict[pid] %} + {% set pdoc=pdict[pid] %} + {% set psdoc=psdict[pid] %} {% if handler.hasPerm(perm.PERM_LOGGEDIN) %} {% if psdoc['rid'] %} @@ -125,17 +127,15 @@ {{ problem.render_problem_title( pdoc, invalid=not tsdoc['enroll'], - show_tags=false, - rp=vj4.job.rp.get_rp_expect(pdoc) if (not psdoc or psdoc['status'] != status.STATUS_ACCEPTED) else none + show_tags=false ) }} {{ pdoc.nSubmit }} {{ (100 * pdoc.nAccept / pdoc.nSubmit)|round|int if pdoc.nSubmit > 0 else _('?') }} {{ pdoc['difficulty'] or _('(None)') }} - {% endwith %} {% else %} - {% with pdoc = {'domain_id': handler.domain_id, 'doc_id': pid, 'hidden': true, 'title': '*'} %} + {% set pdoc = {'_id': pid, 'hidden': true, 'title': '*'} %} @@ -146,7 +146,6 @@ * * - {% endwith %} {% endif %} {% endfor %} @@ -175,8 +174,8 @@ {% endif %} - {% if handler.own(tdoc, perm.PERM_EDIT_TRAINING_SELF) or handler.hasPerm(perm.PERM_EDIT_TRAINING) %} - {% endif %} @@ -192,7 +191,7 @@
{{ _('Status') }}
{% if tsdoc['enroll'] %}{{ _('Completed' if tsdoc['done'] else 'In Progress') }}{% else %}{{ _('Not Enrolled') }}{% endif %}
{% endif %} {% if tsdoc['enroll'] %} -
{{ _('Progress') }}
{{ _('Completed') }} {{ (100 * tsdoc['done_pids']|length / pids|length)|round|int }}%
+
{{ _('Progress') }}
{{ _('Completed') }} {{ (100 * tsdoc['donePids']|length / pids|length)|round|int }}%
{% endif %}
{{ _('Enrollees') }}
{{ tdoc['enroll']|default(0) }}
{{ _('Created By') }}
diff --git a/templates/training_edit.html b/templates/training_edit.html index d24b47c4..2f05fe68 100644 --- a/templates/training_edit.html +++ b/templates/training_edit.html @@ -9,7 +9,7 @@
diff --git a/templates/training_main.html b/templates/training_main.html index e3565e4d..5141605d 100644 --- a/templates/training_main.html +++ b/templates/training_main.html @@ -1,3 +1,4 @@ +{% set page_name = "training_main" %} {% extends "layout/basic.html" %} {% block content %}
@@ -6,7 +7,7 @@

{{ _('All Training Plans') }}

- {% if not tdocs %} + {% if not tdocs.length %} {{ nothing.render('Sorry, there is no training plan.') }} {% else %}
    @@ -15,24 +16,24 @@
    -
    {{ tdoc['enroll']|default(0) }}
    +
    {{ tdoc.enroll|default(0) }}
    {{ _('Enrolled') }}
    -

    {{ tdoc['title'] }}

    +

    {{ tdoc.title }}

    {{ tdoc['content'] }}

    • - {{ _('{0} sections').format(tdoc['dag']|length) }}, {{ _('{0} problems').format(handler.get_pids(tdoc)|length) }} + {{ _('{0} sections').format(tdoc['dag']|length) }}, {{ _('{0} problems').format(model.training.getPids(tdoc)|length) }}
    • - {% if tsdict[tdoc['doc_id']]['enroll'] %} - {% if not tsdict[tdoc['doc_id']]['done'] %} + {% if tsdict[tdoc._id]['enroll'] %} + {% if not tsdict[tdoc._id]['done'] %} - {{ _('Completed') }} {{ (100 * tsdict[tdoc['doc_id']]['done_pids']|length / handler.get_pids(tdoc)|length)|round|int }}% + {{ _('Completed') }} {{ (100 * tsdict[tdoc._id]['done_pids']|length / model.training.getPids(tdoc)|length)|round|int }}% {% else %} {{ _('Completed') }} 100% @@ -59,15 +60,15 @@
      - {% for tsdoc in tsdict.values() %} + {% for tsdoc in tsdict %} {% if tsdoc['enroll'] %}
    1. -

      {{ tdict[tsdoc['doc_id']]['title'] }}

      -
      {{ _('Complete') }} {{ (100 * tsdoc['done_pids']|length / handler.get_pids(tdict[tsdoc['doc_id']])|length)|round|int }}%
      +

      {{ tdict[tsdoc['tid']]['title'] }}

      +
      {{ _('Complete') }} {{ (100 * tsdoc['done_pids']|length / handler.get_pids(tdict[tsdoc['doc_id']])|length)|round|int }}%
    2. {% endif %} @@ -83,7 +84,7 @@