diff --git a/hydro/handler/contest.js b/hydro/handler/contest.js index e99797292..5e65c5369 100644 --- a/hydro/handler/contest.js +++ b/hydro/handler/contest.js @@ -1,6 +1,6 @@ const { ContestNotLiveError, ValidationError, ProblemNotFoundError, - ContestNotAttendedError,ContestScoreboardHiddenError } = require('../error'), + ContestNotAttendedError, ContestScoreboardHiddenError } = require('../error'), { PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD, PERM_CREATE_CONTEST, PERM_EDIT_CONTEST } = require('../permission'), { constants } = require('../options'), @@ -22,7 +22,7 @@ class ContestHandler extends Handler { return false; } canShowScoreboard(tdoc, allowPermOverride = true) { - if (contest.RULES[tdoc.rule].showScoreboardFunc(tdoc, new Date())) return true; + if (contest.RULES[tdoc.rule].showScoreboard(tdoc, new Date())) return true; if (allowPermOverride && this.canViewHiddenScoreboard(tdoc)) return true; return false; } @@ -37,12 +37,17 @@ class ContestHandler extends Handler { return tdoc, rows, udict; } async verifyProblems(pids) { - let pdocs = await problem.getMulti({ _id: { $in: pids } }).sort({ doc_id: 1 }).toArray(); + console.log(pids); + let pdocs = await problem.getMulti({ + $or: [ + { _id: { $in: pids } }, { pid: { $in: pids } } + ] + }).sort({ _id: 1 }).toArray(); if (pids.length != pdocs.length) for (let pid of pids) { let p = false; for (let pdoc of pdocs) - if (pid == pdoc._id) { + if (pid == pdoc._id || pid == pdoc.pid) { p = true; break; } @@ -105,7 +110,7 @@ class ContestDetailHandler extends ContestHandler { }; } async post_attend() { - if (contest.isDone(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id); + if (contest.is_done(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id); await contest.attend(this.tdoc._id, this.uid); this.back(); } @@ -141,7 +146,7 @@ class ContestEditHandler extends ContestDetailHandler { let dt = this.tdoc.beginAt; this.response.body = { rules, tdoc: this.tdoc, duration, path, pids: this.tdoc.pids.join(','), - date_text: `${dt.getFullYear()}-${dt.getMonth()}-${dt.getDate()}`, + date_text: `${dt.getFullYear()}-${dt.getMonth() + 1}-${dt.getDate()}`, time_text: `${dt.getHours()}:${dt.getMinutes()}`, page_name: 'contest_edit' }; @@ -149,7 +154,7 @@ class ContestEditHandler extends ContestDetailHandler { async post({ beginAtDate, beginAtTime, duration, title, content, rule, pids }) { let beginAt, endAt; try { - beginAt = Date.parse(`${beginAtDate}T${beginAtTime}+8:00`); + beginAt = new Date(Date.parse(`${beginAtDate} ${beginAtTime.replace('-', ':')}`)); } catch (e) { throw new ValidationError('beginAtDate', 'beginAtTime'); } @@ -237,19 +242,22 @@ class ContestCreateHandler extends ContestHandler { let dt = new Date(ts); this.response.body = { rules, page_name: 'contest_create', - date_text: `${dt.getFullYear()}-${dt.getMonth()}-${dt.getDate()}`, - time_text: `${dt.getHours()}-${dt.getMinutes()}`, + date_text: `${dt.getFullYear()}-${dt.getMonth() + 1}-${dt.getDate()}`, + time_text: `${dt.getHours()}:${dt.getMinutes()}`, pids: '1000, 1001' }; } async post({ title, content, rule, beginAtDate, beginAtTime, duration, pids }) { let beginAt, endAt; + console.log(beginAtDate, beginAtTime); try { - beginAt = Date.parse(`${beginAtDate}T${beginAtTime}+8:00`); + beginAt = new Date(Date.parse(`${beginAtDate} ${beginAtTime.replace('-', ':')}`)); + console.log(beginAt, duration); } catch (e) { throw new ValidationError('beginAtDate', 'beginAtTime'); } - endAt = new Date(beginAt + duration * 3600 * 1000); + endAt = new Date(beginAt.getTime() + duration * 3600 * 1000); + console.log(endAt); if (beginAt >= endAt) throw new ValidationError('duration'); await this.verifyProblems(pids); let tid = await contest.add(title, content, this.user._id, rule, beginAt, endAt, pids); diff --git a/hydro/handler/problem.js b/hydro/handler/problem.js index a2889f070..58998e0a9 100644 --- a/hydro/handler/problem.js +++ b/hydro/handler/problem.js @@ -52,14 +52,11 @@ class ProblemRandomHandler extends ProblemHandler { } class ProblemDetailHandler extends ProblemHandler { - constructor(ctx) { - super(ctx); - this.uid = ctx.state.user._id; - this.pid = ctx.params.pid; + async _prepare({ pid }) { this.response.template = 'problem_detail.html'; - } - async _prepare() { - if (this.pid) this.pdoc = await problem.get(this); + this.uid = this.user._id; + this.pid = pid; + if (pid) this.pdoc = await problem.get(this); if (this.pdoc.hidden && this.pdoc.owner != this.uid) this.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); if (this.pdoc) this.udoc = await user.getById(this.pdoc.owner); this.response.body = { diff --git a/hydro/lib/misc.js b/hydro/lib/misc.js index 73f8df49d..d07a4bddc 100644 --- a/hydro/lib/misc.js +++ b/hydro/lib/misc.js @@ -2,13 +2,13 @@ const md5 = require('./md5'); exports.gravatar_url = function (email, size) { return `//gravatar.loli.net/avatar/${md5((email || '').toString().trim().toLowerCase())}?d=mm&s=${size}`; }; -exports.datetime_span = function (dt, { relative = true } = {}) { +exports.datetime_span = function (dt, relative = true, format = '%Y-%m-%d %H:%M:%S') { if (dt.generationTime) dt = new Date(dt.generationTime * 1000); else if (typeof dt == 'number' || typeof dt == 'string') dt = new Date(dt); return '{2}'.format( relative ? ' relative' : '', dt.getTime() / 1000, - dt.toLocaleString() + dt.format(format) ); }; exports.paginate = function* (page, num_pages) { diff --git a/hydro/model/contest.js b/hydro/model/contest.js index bc6eeca4a..4a2a7e5d9 100644 --- a/hydro/model/contest.js +++ b/hydro/model/contest.js @@ -12,82 +12,96 @@ const RULES = { acm: require('../module/contest/acm') }; +async function add(title, content, owner, rule, + beginAt = new Date(), endAt = new Date(), pids = [], data = {}) { + validator.checkTitle(title); + validator.checkContent(content); + if (!this.RULES[rule]) throw new ValidationError('rule'); + if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt'); + Object.assign(data, { content, owner, title, rule, beginAt, endAt, pids, attend: 0 }); + this.RULES[rule].check(data); + let res = await coll.insertOne(data); + return res.insertedId; +} +async function edit(tid, $set) { + if ($set.title) validator.checkTitle($set.title); + if ($set.content) validator.checkIntro($set.content); + if ($set.rule) + if (!this.RULES[$set.rule]) throw new ValidationError('rule'); + if ($set.beginAt && $set.endAt) + if ($set.beginAt >= $set.endAt) throw new ValidationError('beginAt', 'endAt'); + let tdoc = await coll.findOne({ tid }); + if (!tdoc) throw new ContestNotFoundError(tid); + this.RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set)); + await coll.findOneAndUpdate({ tid }, { $set }); + return tdoc; +} +async function get(tid) { + let tdoc = await coll.findOne({ _id: tid }); + if (!tdoc) throw new ContestNotFoundError(tid); + return tdoc; +} +async function get_random_id(query) { + let pdocs = coll.find(query); + let pcount = await pdocs.count(); + if (pcount) { + let pdoc = await pdocs.skip(Math.floor(Math.random() * pcount)).limit(1).toArray()[0]; + return pdoc.pid; + } else return null; +} +async function getListStatus(uid, tids) { + let r = {}; + for (let tid of tids) r[tid] = await this.getStatus(tid, uid); + return r; +} +async function attend(tid, uid) { + try { + await coll_status.insertOne({ tid, uid }); + } catch (e) { + throw new ContestAlreadyAttendedError(tid, uid); + } + await coll.findOneAndUpdate({ _id: tid }, { $inc: { attend: 1 } }); +} +function is_new(tdoc, days = 1) { + let now = new Date().getTime(); + let readyAt = tdoc.beginAt.getTime(); + return (now < readyAt - days * 24 * 3600 * 1000); +} +function is_upcoming(tdoc, days = 1) { + let now = new Date().getTime(); + let readyAt = tdoc.beginAt.getTime(); + return (now > readyAt + days * 24 * 3600 * 1000 && now < tdoc.beginAt); +} +function is_not_started(tdoc) { + return new Date() < tdoc.beginAt; +} +function is_ongoing(tdoc) { + let now = new Date(); + return (tdoc.beginAt <= now && now < tdoc.endAt); +} +function is_done(tdoc) { + return tdoc.endAt <= new Date(); +} module.exports = { - RULES, - async add(title, content, owner, rule, - beginAt = new Date(), endAt = new Date(), pids = [], data) { - validator.checkTitle(title); - validator.checkContent(content); - if (!this.RULES[rule]) throw new ValidationError('rule'); - if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt'); - Object.assign(data, { content, owner, title, rule, beginAt, endAt, pids, attend: 0 }); - this.RULES[rule].check(data); - return await coll.insertOne(data); - }, + RULES, add, getListStatus, attend, edit, get, get_random_id, count: query => coll.find(query).count(), - async edit(tid, $set) { - if ($set.title) validator.checkTitle($set.title); - if ($set.content) validator.checkIntro($set.content); - if ($set.rule) - if (!this.RULES[$set.rule]) throw new ValidationError('rule'); - if ($set.beginAt && $set.endAt) - if ($set.beginAt >= $set.endAt) throw new ValidationError('beginAt', 'endAt'); - let tdoc = await coll.findOne({ tid }); - if (!tdoc) throw new ContestNotFoundError(tid); - this.RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set)); - await coll.findOneAndUpdate({ tid }, { $set }); - return tdoc; - }, - async get(tid) { - let tdoc = await coll.findOne({ tid }); - if (!tdoc) throw new ContestNotFoundError(tid); - return tdoc; - }, getMulti: query => coll.find(query), getMultiStatus: query => coll_status.find(query), - async get_random_id(query) { - let pdocs = coll.find(query); - let pcount = await pdocs.count(); - if (pcount) { - let pdoc = await pdocs.skip(Math.floor(Math.random() * pcount)).limit(1).toArray()[0]; - return pdoc.pid; - } else return null; - }, getStatus: (tid, uid) => coll_status.findOne({ tid, uid }), setStatus: (tid, uid, $set) => coll_status.findOneAndUpdate({ tid, uid }, { $set }), - async getListStatus(uid, tids) { - let r = {}; - for (let tid of tids) r[tid] = await this.getStatus(tid, uid); - return r; - }, - is_new(tdoc, days = 1) { - let now = new Date().getTime(); - let readyAt = tdoc.beginAt.getTime(); - return (now < readyAt - days * 24 * 3600 * 1000); - }, - is_upcoming(tdoc, days = 1) { - let now = new Date().getTime(); - let readyAt = tdoc.beginAt.getTime(); - return (now > readyAt + days * 24 * 3600 * 1000 && now < tdoc.beginAt); - }, - is_not_started: tdoc => new Date() < tdoc.beginAt, - is_ongoing(tdoc) { - let now = new Date(); - return (tdoc.beginAt <= now && now < tdoc.endAt); - }, - is_done: tdoc => tdoc.endAt <= new Date(), + is_new, is_upcoming, is_not_started, is_ongoing, is_done, status_text: tdoc => - this.is_new(tdoc) + is_new(tdoc) ? 'New' - : this.is_upcoming(tdoc) + : is_upcoming(tdoc) ? 'Ready (☆▽☆)' - : this.is_ongoing(tdoc) + : is_ongoing(tdoc) ? 'Live...' : 'Done', get_status: tdoc => - this.is_not_started(tdoc) + is_not_started(tdoc) ? 'not_started' - : this.is_ongoing(tdoc) + : is_ongoing(tdoc) ? 'ongoing' : 'finished', }; diff --git a/hydro/model/problem.js b/hydro/model/problem.js index 6a7afeb68..7f48f6b17 100644 --- a/hydro/model/problem.js +++ b/hydro/model/problem.js @@ -85,6 +85,11 @@ async function random(query) { return pdoc.pid; } else return null; } +async function getList(pids) { + let r = {}; + for (let pid of pids) r[pid] = await get({pid}); + return r; +} module.exports = { add, @@ -94,5 +99,6 @@ module.exports = { count, random, getById, - getMulti + getMulti, + getList }; \ No newline at end of file diff --git a/hydro/module/contest/oi.js b/hydro/module/contest/oi.js index 1a4c8c402..6ee27cd7c 100644 --- a/hydro/module/contest/oi.js +++ b/hydro/module/contest/oi.js @@ -10,6 +10,9 @@ module.exports = { } return { score, detail }; }, + showScoreboard(tdoc,now){ + return now>tdoc.endAt; + }, scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, dudict, pdict) { let columns = [ { type: 'rank', value: _('Rank') }, diff --git a/hydro/service/server.js b/hydro/service/server.js index 212b2a4d6..5e866f835 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -13,8 +13,7 @@ const blacklist = require('../model/blacklist'), token = require('../model/token'), opcount = require('../model/opcount'), - { UserNotFoundError, BlacklistedError, PermissionError, - NotFoundError } = require('../error'); + { UserNotFoundError, BlacklistedError, PermissionError } = require('../error'); const options = require('../options'); let http = options.listen.https ? require('https') : require('http'); @@ -128,40 +127,33 @@ class Handler { async ___cleanup() { try { await this.renderBody(); - } catch (e) { - try { - let error; - if (this.response.body && this.response.body.error) error = this.response.body.error; - else error = e; - if (error instanceof NotFoundError) this.response.status = 404; - if (error.name == 'Template render error') throw error; - if (this.preferJson) this.response.body = { error }; - else await this.render('error.html', { error }); - } catch (error) { - if (this.preferJson) this.response.body = { error }; - else await this.render('bsod.html', { error }); - } + } catch (error) { + if (this.preferJson) this.response.body = { error }; + else await this.render('bsod.html', { error }); } await this.putResponse(); await this.saveCookie(); } async renderBody() { - if (this.response.body || this.response.template) { - if (this.request.query.noTemplate || this.preferJson) return; - if (this.request.query.template || this.response.template) { - this.response.body = this.response.body || {}; - await this.render(this.request.query.template || this.response.template, this.response.body); + if (!this.preferJson) + if (this.response.body || this.response.template) { + if (this.request.query.noTemplate || this.preferJson) return; + if (this.request.query.template || this.response.template) { + this.response.body = this.response.body || {}; + await this.render(this.request.query.template || this.response.template, this.response.body); + } } - } } async putResponse() { - if (this.response.redirect) { - this.response.type = 'application/octet-stream'; - this.redirect(this.response.redirect); + if (this.response.redirect && !this.preferJson) { + this.ctx.response.type = 'application/octet-stream'; + this.ctx.response.status = 302; + this.ctx.redirect(this.response.redirect); + } else { + if (this.response.body) this.ctx.body = this.response.body; + if (this.response.type) this.ctx.response.type = this.response.type; + if (this.response.status) this.ctx.response.status = this.response.status; } - if (this.response.body) this.ctx.body = this.response.body; - if (this.response.type) this.ctx.response.type = this.response.type; - if (this.response.status) this.ctx.response.status = this.response.status; } async saveCookie() { if (this.session.sid) @@ -184,9 +176,11 @@ class Handler { } this.ctx.cookies.set('sid', this.session.sid, cookie); } - async onerror(e) { - console.error(e.message, e.params); - console.error(e.stack); + async onerror(error) { + console.error(error.message, error.params); + console.error(error.stack); + this.response.template = 'error.html'; + this.response.body = { error }; await this.___cleanup(); } } @@ -203,6 +197,8 @@ function Route(route, handler) { if (args.page) args.page = parseInt(args.page); if (args.rid) args.rid = new ObjectID(args.rid); if (args.tid) args.tid = new ObjectID(args.tid); + if (args.duration) args.duration = parseFloat(args.duration); + if (args.pids) args.pids = args.pids.split(',').map(i => i.trim()); if (h.___prepare) await h.___prepare(args); if (h.__prepare) await h.__prepare(args); @@ -330,6 +326,7 @@ exports.Connection = Connection; exports.start = function start() { app.use(morgan(':method :url :status :res[content-length] - :response-time ms')); app.use(router.routes()).use(router.allowedMethods()); + Route('*', Handler); server.listen(options.listen.port); console.log('Server listening at: %s', options.listen.port); }; diff --git a/hydro/utils.js b/hydro/utils.js index 7f8e38e7e..27a0d19c6 100644 --- a/hydro/utils.js +++ b/hydro/utils.js @@ -27,3 +27,14 @@ Array.isDiff = function isDiff(a, b) { if (a[i] != b[i]) return true; return false; }; + +Date.prototype.format = function (fmt = '%Y-%m-%d %H:%M:%S') { + return fmt + .replace('%Y', this.getFullYear()) + .replace('%m', this.getMonth() + 1) + .replace('%D', this.getDate()) + .replace('%d', this.getDate()) + .replace('%H', this.getHours()) + .replace('%M', this.getMinutes()) + .replace('%S', this.getSeconds()); +}; diff --git a/templates/components/contest.html b/templates/components/contest.html index 5faf27d95..1f8234b71 100644 --- a/templates/components/contest.html +++ b/templates/components/contest.html @@ -1,8 +1,8 @@ {% import "components/user.html" as user with context %} {% macro render_time(tdoc_time) %} -{{ datetime_span(tdoc_time, false, '%Y-%m-%d %H:%M') }} +{{ datetime_span(tdoc_time, false, '%Y-%m-%d %H:%M')|safe }} {% endmacro %} {% macro render_duration(tdoc) %} -{{ ((tdoc['end_at'] - tdoc['begin_at']).total_seconds() / 3600)|round(1) }} +{{ ((tdoc.endAt.getTime() - tdoc.beginAt.getTime()) /1000 / 3600)|round(1) }} {% endmacro %} diff --git a/templates/contest_detail.html b/templates/contest_detail.html index 23755862b..c83e39847 100644 --- a/templates/contest_detail.html +++ b/templates/contest_detail.html @@ -1,3 +1,4 @@ +{% set page_name = "contest_detail" %} {% extends "layout/basic.html" %} {% import "components/contest.html" as contest with context %} {% import "components/record.html" as record with context %} @@ -7,20 +8,20 @@
{% endif %}{{ _('We will check code similarity after this contest.') }}
| {{ _('Submitted') }} | {% endif %} -{{ datetime_span(rdoc['_id'].generationTime) }} | - {% endwith %} +{{ datetime_span(rdoc._id) }} | {% else %}{{ _('No Submissions') }} | - | @@ -73,9 +73,8 @@ {% endif %}