mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
Contest rule: ACM/ICPC
This commit is contained in:
1 parent
d1ec9064fc
commit
e06283c0cc
43 files changed
+158
-891
No files matched your search
@@ -69,7 +69,7 @@ class ContestDetailHandler extends ContestHandler {
|
||||
}
|
||||
|
||||
async postAttend() {
|
||||
if (contest.is_done(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id);
|
||||
if (contest.isDone(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id);
|
||||
await contest.attend(this.tdoc._id, this.user._id);
|
||||
this.back();
|
||||
}
|
||||
@@ -88,6 +88,17 @@ class ContestScoreboardHandler extends ContestDetailHandler {
|
||||
};
|
||||
}
|
||||
}
|
||||
class ContestScoreboardDownloadHandler extends ContestDetailHandler {
|
||||
async get({ tid, ext }) {
|
||||
const getContent = {
|
||||
csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`,
|
||||
html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }),
|
||||
};
|
||||
if (!getContent[ext]) throw new ValidationError('ext');
|
||||
const [, rows] = await this.getScoreboard(tid, true);
|
||||
this.binary(await getContent[ext](rows), `${this.tdoc.title}.${ext}`);
|
||||
}
|
||||
}
|
||||
class ContestEditHandler extends ContestDetailHandler {
|
||||
async prepare({ tid }) {
|
||||
const tdoc = await contest.get(tid);
|
||||
@@ -150,9 +161,9 @@ class ContestProblemHandler extends ContestDetailHandler {
|
||||
]);
|
||||
this.attended = this.tsdoc && this.tsdoc.attend === 1;
|
||||
this.response.template = 'problem_detail.html';
|
||||
if (contest.is_done(this.tdoc)) {
|
||||
if (contest.isDone(this.tdoc)) {
|
||||
if (!this.attended) throw new ContestNotAttendedError(this.tdoc._id);
|
||||
if (!contest.is_ongoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id);
|
||||
if (!contest.isOngoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id);
|
||||
}
|
||||
console.log(this.tdoc.pids, this.pdoc._id);
|
||||
if (!this.tdoc.pids.map((s) => s.toString()).includes(this.pdoc._id.toString())) {
|
||||
@@ -260,6 +271,7 @@ Route('/c', ContestListHandler);
|
||||
Route('/c/:tid', ContestDetailHandler);
|
||||
Route('/c/:tid/edit', ContestEditHandler);
|
||||
Route('/c/:tid/scoreboard', ContestScoreboardHandler);
|
||||
Route('/c/:tid/export/:ext', ContestScoreboardDownloadHandler);
|
||||
Route('/c/:tid/p/:pid', ContestProblemHandler);
|
||||
Route('/c/:tid/p/:pid/submit', ContestProblemSubmitHandler);
|
||||
Route('/contest/create', ContestCreateHandler);
|
||||
+23
-271
@@ -83,9 +83,12 @@ async function updateStatus(tid, uid, rid, pid, accept, score) {
|
||||
}, { upsert: true });
|
||||
if (!tsdoc.value.attend) throw new ContestNotAttendedError(tid, uid);
|
||||
}
|
||||
function getStatus(tid, uid) {
|
||||
return collStatus.findOne({ tid, uid });
|
||||
}
|
||||
async function getListStatus(uid, tids) {
|
||||
const r = {};
|
||||
for (const tid of tids) r[tid] = await this.getStatus(tid, uid); // eslint-disable-line no-await-in-loop
|
||||
for (const tid of tids) r[tid] = await getStatus(tid, uid); // eslint-disable-line no-await-in-loop
|
||||
return r;
|
||||
}
|
||||
async function attend(tid, uid) {
|
||||
@@ -99,24 +102,24 @@ async function attend(tid, uid) {
|
||||
function getMultiStatus(query) {
|
||||
return collStatus.find(query);
|
||||
}
|
||||
function is_new(tdoc, days = 1) {
|
||||
function isNew(tdoc, days = 1) {
|
||||
const now = new Date().getTime();
|
||||
const readyAt = tdoc.beginAt.getTime();
|
||||
return (now < readyAt - days * 24 * 3600 * 1000);
|
||||
}
|
||||
function is_upcoming(tdoc, days = 1) {
|
||||
function isUpcoming(tdoc, days = 1) {
|
||||
const now = new Date().getTime();
|
||||
const readyAt = tdoc.beginAt.getTime();
|
||||
return (now > readyAt - days * 24 * 3600 * 1000 && now < tdoc.beginAt);
|
||||
}
|
||||
function is_not_started(tdoc) {
|
||||
function isNotStarted(tdoc) {
|
||||
return (new Date()) < tdoc.beginAt;
|
||||
}
|
||||
function is_ongoing(tdoc) {
|
||||
function isOngoing(tdoc) {
|
||||
const now = new Date();
|
||||
return (tdoc.beginAt <= now && now < tdoc.endAt);
|
||||
}
|
||||
function is_done(tdoc) {
|
||||
function isDone(tdoc) {
|
||||
return tdoc.endAt <= new Date();
|
||||
}
|
||||
|
||||
@@ -169,168 +172,38 @@ module.exports = {
|
||||
edit,
|
||||
get,
|
||||
updateStatus,
|
||||
getStatus,
|
||||
count: (query) => coll.find(query).count(),
|
||||
getMulti: (query) => coll.find(query),
|
||||
getStatus: (tid, uid) => coll_status.findOne({ tid, uid }),
|
||||
setStatus: (tid, uid, $set) => coll_status.findOneAndUpdate({ tid, uid }, { $set }),
|
||||
is_new,
|
||||
is_upcoming,
|
||||
is_not_started,
|
||||
is_ongoing,
|
||||
is_done,
|
||||
status_text: (tdoc) => (
|
||||
is_new(tdoc)
|
||||
setStatus: (tid, uid, $set) => collStatus.findOneAndUpdate({ tid, uid }, { $set }),
|
||||
isNew,
|
||||
isUpcoming,
|
||||
isNotStarted,
|
||||
isOngoing,
|
||||
isDone,
|
||||
statusText: (tdoc) => (
|
||||
isNew(tdoc)
|
||||
? 'New'
|
||||
: is_upcoming(tdoc)
|
||||
: isUpcoming(tdoc)
|
||||
? 'Ready (☆▽☆)'
|
||||
: is_ongoing(tdoc)
|
||||
: isOngoing(tdoc)
|
||||
? 'Live...'
|
||||
: 'Done'),
|
||||
get_status: (tdoc) => (
|
||||
is_not_started(tdoc)
|
||||
getStatusText: (tdoc) => (
|
||||
isNotStarted(tdoc)
|
||||
? 'not_started'
|
||||
: is_ongoing(tdoc)
|
||||
: isOngoing(tdoc)
|
||||
? 'ongoing'
|
||||
: 'finished'),
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
journal_key_func = lambda j: j['rid']
|
||||
|
||||
Rule = collections.namedtuple('Rule', ['show_record_func',
|
||||
'show_scoreboard_func',
|
||||
'stat_func',
|
||||
'status_sort',
|
||||
'rank_func',
|
||||
'scoreboard_func'])
|
||||
|
||||
def _oi_equ_func(a, b):
|
||||
return a.get('score', 0) == b.get('score', 0)
|
||||
|
||||
RULES = {
|
||||
constant.contest.RULE_OI: Rule(lambda tdoc, now: now > tdoc['end_at'],
|
||||
lambda tdoc, now: now > tdoc['end_at'],
|
||||
_oi_stat,
|
||||
[('score', -1)],
|
||||
functools.partial(rank.ranked, equ_func=_oi_equ_func),
|
||||
_oi_scoreboard),
|
||||
constant.contest.RULE_ACM: Rule(lambda tdoc, now: now >= tdoc['begin_at'],
|
||||
lambda tdoc, now: now >= tdoc['begin_at'],
|
||||
_acm_stat,
|
||||
[('accept', -1), ('time', 1)],
|
||||
functools.partial(enumerate, start=1),
|
||||
_acm_scoreboard),
|
||||
constant.contest.RULE_ASSIGNMENT: Rule(lambda tdoc, now: now >= tdoc['begin_at'],
|
||||
lambda tdoc, now: False, # TODO: show scoreboard according to assignment preference
|
||||
_assignment_stat,
|
||||
[('penalty_score', -1), ('time', 1)],
|
||||
functools.partial(enumerate, start=1),
|
||||
_assignment_scoreboard),
|
||||
}
|
||||
|
||||
def get_multi(domainId: str, doc_type: int, fields=None, **kwargs):
|
||||
# TODO(twd2): projection.
|
||||
if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]:
|
||||
raise error.InvalidArgumentError('doc_type')
|
||||
return document.get_multi(domainId=domainId,
|
||||
doc_type=doc_type,
|
||||
fields=fields,
|
||||
**kwargs) \
|
||||
.sort([('doc_id', -1)])
|
||||
|
||||
|
||||
@argmethod.wrap
|
||||
async def attend(domainId: str, doc_type: int, cid: objeccid.Objeccid, uid: int):
|
||||
# TODO(iceboy): check time.
|
||||
if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]:
|
||||
raise error.InvalidArgumentError('doc_type')
|
||||
try:
|
||||
await document.capped_inc_status(domainId, doc_type, cid,
|
||||
uid, 'attend', 1, 0, 1)
|
||||
except errors.DuplicateKeyError:
|
||||
if doc_type == document.TYPE_CONTEST:
|
||||
raise error.ContestAlreadyAttendedError(domainId, cid, uid) from None
|
||||
elif doc_type == document.TYPE_HOMEWORK:
|
||||
raise error.HomeworkAlreadyAttendedError(domainId, cid, uid) from None
|
||||
return await document.inc(domainId, doc_type, cid, 'attend', 1)
|
||||
|
||||
|
||||
@argmethod.wrap
|
||||
async def get_status(domainId: str, doc_type: int, cid: objeccid.Objeccid, uid: int, fields=None):
|
||||
if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]:
|
||||
raise error.InvalidArgumentError('doc_type')
|
||||
return await document.get_status(domainId, doc_type, doc_id=cid,
|
||||
uid=uid, fields=fields)
|
||||
|
||||
|
||||
def get_multi_status(doc_type: int, *, fields=None, **kwargs):
|
||||
if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]:
|
||||
raise error.InvalidArgumentError('doc_type')
|
||||
return document.get_multi_status(doc_type=doc_type,
|
||||
fields=fields, **kwargs)
|
||||
|
||||
|
||||
async def get_dict_status(domainId, uid, doc_type, cids, *, fields=None):
|
||||
if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]:
|
||||
raise error.InvalidArgumentError('doc_type')
|
||||
result = dict()
|
||||
async for tsdoc in get_multi_status(domainId=domainId,
|
||||
uid=uid,
|
||||
doc_type=doc_type,
|
||||
doc_id={'$in': list(set(cids))},
|
||||
fields=fields):
|
||||
result[tsdoc['doc_id']] = tsdoc
|
||||
return result
|
||||
|
||||
|
||||
@argmethod.wrap
|
||||
async def get_and_list_status(domainId: str, doc_type: int, cid: objeccid.Objeccid, fields=None):
|
||||
# TODO(iceboy): projection, pagination.
|
||||
if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]:
|
||||
raise error.InvalidArgumentError('doc_type')
|
||||
tdoc = await get(domainId, doc_type, cid)
|
||||
tsdocs = await document.get_multi_status(domainId=domainId,
|
||||
doc_type=doc_type,
|
||||
doc_id=tdoc['doc_id'],
|
||||
fields=fields) \
|
||||
.sort(RULES[tdoc['rule']].status_sort) \
|
||||
.to_list()
|
||||
return tdoc, tsdocs
|
||||
|
||||
|
||||
def _get_status_journal(tsdoc):
|
||||
# Sort and uniquify journal of the contest status document, by rid.
|
||||
return [list(g)[-1] for _, g in itertools.groupby(sorted(tsdoc['journal'], key=journal_key_func),
|
||||
key=journal_key_func)]
|
||||
|
||||
|
||||
@argmethod.wrap
|
||||
async def update_status(domainId: str, doc_type: int, cid: objeccid.Objeccid, uid: int,
|
||||
rid: objeccid.Objeccid, pid: document.convert_doc_id,
|
||||
accept: bool, score: int):
|
||||
"""This method returns None when the modification has been superseded by a parallel operation."""
|
||||
if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]:
|
||||
raise error.InvalidArgumentError('doc_type')
|
||||
tdoc = await document.get(domainId, doc_type, cid)
|
||||
tsdoc = await document.rev_push_status(
|
||||
domainId, tdoc['doc_type'], tdoc['doc_id'], uid,
|
||||
'journal', {'rid': rid, 'pid': pid, 'accept': accept, 'score': score})
|
||||
if 'attend' not in tsdoc or not tsdoc['attend']:
|
||||
if tdoc['doc_type'] == document.TYPE_CONTEST:
|
||||
raise error.ContestNotAttendedError(domainId, cid, uid)
|
||||
elif tdoc['doc_type'] == document.TYPE_HOMEWORK:
|
||||
raise error.HomeworkNotAttendedError(domainId, cid, uid)
|
||||
else:
|
||||
raise error.InvalidArgumentError('doc_type')
|
||||
|
||||
journal = _get_status_journal(tsdoc)
|
||||
stats = RULES[tdoc['rule']].stat_func(tdoc, journal)
|
||||
tsdoc = await document.rev_set_status(domainId, tdoc['doc_type'], cid, uid, tsdoc['rev'],
|
||||
journal=journal, **stats)
|
||||
return tsdoc
|
||||
|
||||
|
||||
@argmethod.wrap
|
||||
async def recalc_status(domainId: str, doc_type: int, cid: objeccid.Objeccid):
|
||||
if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]:
|
||||
@@ -346,125 +219,4 @@ async def recalc_status(domainId: str, doc_type: int, cid: objeccid.Objeccid):
|
||||
stats = RULES[tdoc['rule']].stat_func(tdoc, journal)
|
||||
await document.rev_set_status(domainId, doc_type, cid, tsdoc['uid'], tsdoc['rev'],
|
||||
return_doc=False, journal=journal, **stats)
|
||||
|
||||
|
||||
def _parse_pids(pids_str):
|
||||
pids = misc.dedupe(map(document.convert_doc_id, pids_str.split(',')))
|
||||
return pids
|
||||
|
||||
|
||||
def _format_pids(pids_list):
|
||||
return ','.join([str(pid) for pid in pids_list])
|
||||
|
||||
|
||||
class ContestStatusMixin(object):
|
||||
@property
|
||||
@functools.lru_cache()
|
||||
def now(self):
|
||||
# TODO(iceboy): This does not work on multi-machine environment.
|
||||
return datetime.datetime.utcnow()
|
||||
|
||||
def is_new(self, tdoc):
|
||||
ready_at = tdoc['begin_at'] - datetime.timedelta(days=1)
|
||||
return self.now < ready_at
|
||||
|
||||
def is_upcoming(self, tdoc):
|
||||
ready_at = tdoc['begin_at'] - datetime.timedelta(days=1)
|
||||
return ready_at <= self.now < tdoc['begin_at']
|
||||
|
||||
def is_not_started(self, tdoc):
|
||||
return self.now < tdoc['begin_at']
|
||||
|
||||
def is_ongoing(self, tdoc):
|
||||
return tdoc['begin_at'] <= self.now < tdoc['end_at']
|
||||
|
||||
def is_done(self, tdoc):
|
||||
return tdoc['end_at'] <= self.now
|
||||
|
||||
def is_homework_extended(self, tdoc):
|
||||
return tdoc['penalty_since'] <= self.now < tdoc['end_at']
|
||||
|
||||
def status_text(self, tdoc):
|
||||
if self.is_new(tdoc):
|
||||
return 'New'
|
||||
elif self.is_upcoming(tdoc):
|
||||
return 'Ready (☆▽☆)'
|
||||
elif self.is_ongoing(tdoc):
|
||||
return 'Live...'
|
||||
else:
|
||||
return 'Done'
|
||||
|
||||
def get_status(self, tdoc):
|
||||
if self.is_not_started(tdoc):
|
||||
return 'not_started'
|
||||
elif self.is_ongoing(tdoc):
|
||||
return 'ongoing'
|
||||
else:
|
||||
return 'finished'
|
||||
|
||||
|
||||
class ContestVisibilityMixin(object):
|
||||
def can_view_hidden_scoreboard(self, tdoc):
|
||||
if self.domainId != tdoc['domainId']:
|
||||
return False
|
||||
if tdoc['doc_type'] == document.TYPE_CONTEST:
|
||||
return self.hasPerm(builtin.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD)
|
||||
elif tdoc['doc_type'] == document.TYPE_HOMEWORK:
|
||||
return self.hasPerm(builtin.PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD)
|
||||
else:
|
||||
return False
|
||||
|
||||
def can_show_record(self, tdoc, allow_perm_override=True):
|
||||
if RULES[tdoc['rule']].show_record_func(tdoc, datetime.datetime.utcnow()):
|
||||
return True
|
||||
if allow_perm_override and self.can_view_hidden_scoreboard(tdoc):
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_show_scoreboard(self, tdoc, allow_perm_override=True):
|
||||
if RULES[tdoc['rule']].show_scoreboard_func(tdoc, datetime.datetime.utcnow()):
|
||||
return True
|
||||
if allow_perm_override and self.can_view_hidden_scoreboard(tdoc):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ContestCommonOperationMixin(object):
|
||||
async def get_scoreboard(self, doc_type: int, cid: objeccid.Objeccid, is_export: bool=False):
|
||||
if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]:
|
||||
raise error.InvalidArgumentError('doc_type')
|
||||
tdoc, tsdocs = await get_and_list_status(self.domainId, doc_type, cid)
|
||||
if not self.can_show_scoreboard(tdoc):
|
||||
if doc_type == document.TYPE_CONTEST:
|
||||
raise error.ContestScoreboardHiddenError(self.domainId, cid)
|
||||
elif doc_type == document.TYPE_HOMEWORK:
|
||||
raise error.HomeworkScoreboardHiddenError(self.domainId, cid)
|
||||
udict, dudict, pdict = await asyncio.gather(
|
||||
user.get_dict([tsdoc['uid'] for tsdoc in tsdocs]),
|
||||
domain.get_dict_user_by_uid(self.domainId, [tsdoc['uid'] for tsdoc in tsdocs]),
|
||||
problem.get_dict(self.domainId, tdoc['pids']))
|
||||
ranked_tsdocs = RULES[tdoc['rule']].rank_func(tsdocs)
|
||||
rows = RULES[tdoc['rule']].scoreboard_func(is_export, self.translate, tdoc,
|
||||
ranked_tsdocs, udict, dudict, pdict)
|
||||
return tdoc, rows, udict
|
||||
|
||||
async def verify_problems(self, pids):
|
||||
pdocs = await problem.get_multi(domainId=self.domainId, doc_id={'$in': pids},
|
||||
fields={'doc_id': 1}) \
|
||||
.sort('doc_id', 1) \
|
||||
.to_list()
|
||||
exist_pids = [pdoc['_id'] for pdoc in pdocs]
|
||||
if len(pids) != len(exist_pids):
|
||||
for pid in pids:
|
||||
if pid not in exist_pids:
|
||||
raise error.ProblemNotFoundError(self.domainId, pid)
|
||||
return pids
|
||||
|
||||
async def hide_problems(self, pids):
|
||||
await asyncio.gather(*[problem.set_hidden(self.domainId, pid, True) for pid in pids])
|
||||
|
||||
|
||||
class ContestMixin(ContestStatusMixin, ContestVisibilityMixin, ContestCommonOperationMixin):
|
||||
pass
|
||||
|
||||
*/
|
||||
@@ -7,8 +7,8 @@ async function get(_id) {
|
||||
if (doc) return doc.value;
|
||||
return null;
|
||||
}
|
||||
async function update(_id, update, config) {
|
||||
await coll.findOneAndUpdate({ _id }, update, config);
|
||||
async function update(_id, operation, config) {
|
||||
await coll.findOneAndUpdate({ _id }, operation, config);
|
||||
return get(_id);
|
||||
}
|
||||
/**
|
||||
|
||||
+31
-24
@@ -4,36 +4,42 @@ module.exports = {
|
||||
showScoreboard: () => true,
|
||||
showRecord: (tdoc, now) => now > tdoc.endAt,
|
||||
stat: (tdoc, journal) => {
|
||||
const naccept = {}; const effective = {}; const detail = []; let accept = 0; let time = 0;
|
||||
const naccept = {};
|
||||
const effective = {};
|
||||
const detail = [];
|
||||
let accept = 0;
|
||||
let time = 0;
|
||||
for (const j in journal) {
|
||||
if (tdoc.pids.includes(j.pid) && !(effective.includes(j.pid) && effective[j.pid].accept)) { effective[j.pid] = j; }
|
||||
if (!j.accept) { naccept[j.pid]++; }
|
||||
if (tdoc.pids.includes(j.pid)
|
||||
&& !(effective.includes(j.pid) && effective[j.pid].accept)) {
|
||||
effective[j.pid] = j;
|
||||
}
|
||||
if (!j.accept) naccept[j.pid]++;
|
||||
}
|
||||
function _time(jdoc) {
|
||||
const real = jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000);
|
||||
const penalty = 20 * 60 * naccept[jdoc.pid];
|
||||
return real + penalty;
|
||||
}
|
||||
for (const j of effective) { detail.push({ ...j, naccept: naccept[j.pid], time: _time(j) }); }
|
||||
for (const j of effective) detail.push({ ...j, naccept: naccept[j.pid], time: _time(j) });
|
||||
for (const d of detail) {
|
||||
accept += d.accept;
|
||||
if (d.accept) time += d.time;
|
||||
}
|
||||
return { accept, time, detail };
|
||||
},
|
||||
scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, dudict, pdict) {
|
||||
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
|
||||
const columns = [
|
||||
{ type: 'rank', value: _('Rank') },
|
||||
{ type: 'user', value: _('User') },
|
||||
{ type: 'display_name', value: _('Display Name') },
|
||||
{ type: 'solved_problems', value: _('Solved Problems') },
|
||||
];
|
||||
if (is_export) {
|
||||
if (isExport) {
|
||||
columns.push({ type: 'total_time', value: _('Total Time (Seconds)') });
|
||||
columns.push({ type: 'total_time_str', value: _('Total Time') });
|
||||
}
|
||||
for (const i in tdoc.pids) {
|
||||
if (is_export) {
|
||||
if (isExport) {
|
||||
columns.push({
|
||||
type: 'problem_flag',
|
||||
value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title),
|
||||
@@ -55,44 +61,45 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
const rows = [columns];
|
||||
for (const [rank, tsdoc] of ranked_tsdocs) {
|
||||
for (const [rank, tsdoc] of rankedTsdocs) {
|
||||
const tsddict = {};
|
||||
if (tdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; }
|
||||
const row = [];
|
||||
row.push(
|
||||
{ type: 'string', value: rank },
|
||||
{ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] },
|
||||
{ type: 'display_name', value: (dudict[tsdoc.uid] || {}).display_name || '' },
|
||||
{ type: 'string', value: tsdoc.accept || 0 },
|
||||
);
|
||||
if (is_export) {
|
||||
if (isExport) {
|
||||
row.push(
|
||||
{ type: 'string', value: tsdoc.time || 0.0 },
|
||||
{ type: 'string', value: tsdoc.time || 0.0 },
|
||||
);
|
||||
}
|
||||
for (const pid of tdoc.pids) {
|
||||
let rdoc; let col_accepted; let col_time; let
|
||||
col_time_str;
|
||||
let rdoc;
|
||||
let colAccepted;
|
||||
let colTime;
|
||||
let colTimeStr;
|
||||
if ((tsddict[pid] || {}).accept) {
|
||||
rdoc = tsddict[pid].rid;
|
||||
col_accepted = _('Accepted');
|
||||
col_time = tsddict[pid].time;
|
||||
col_time_str = col_time;
|
||||
colAccepted = _('Accepted');
|
||||
colTime = tsddict[pid].time;
|
||||
colTimeStr = colTime;
|
||||
} else {
|
||||
rdoc = null;
|
||||
col_accepted = '-';
|
||||
col_time = '-';
|
||||
col_time_str = '-';
|
||||
colAccepted = '-';
|
||||
colTime = '-';
|
||||
colTimeStr = '-';
|
||||
}
|
||||
if (is_export) {
|
||||
row.push({ type: 'string', value: col_accepted });
|
||||
row.push({ type: 'string', value: col_time });
|
||||
row.push({ type: 'string', value: col_time_str });
|
||||
if (isExport) {
|
||||
row.push({ type: 'string', value: colAccepted });
|
||||
row.push({ type: 'string', value: colTime });
|
||||
row.push({ type: 'string', value: colTimeStr });
|
||||
} else {
|
||||
row.push({
|
||||
type: 'record',
|
||||
value: '{0}\n{1}'.format(col_accepted, col_time_str),
|
||||
value: '{0}\n{1}'.format(colAccepted, colTimeStr),
|
||||
raw: rdoc,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,45 +1,40 @@
|
||||
const { ValidationError } = require('../../error');
|
||||
|
||||
module.exports = {
|
||||
TEXT: 'Assignment',
|
||||
check(data) {
|
||||
if (!data.penalty_since) throw new ValidationError('penalty_since');
|
||||
if (data.penalty_since < data.begin_at) throw new ValidationError('penalty_since', 'begin_at');
|
||||
if (data.penalty_since > data.end_at) throw new ValidationError('penalty_since', 'end_at');
|
||||
},
|
||||
check() {},
|
||||
stat(tdoc, journal) {
|
||||
const effective = {};
|
||||
const detail = [];
|
||||
let score = 0;
|
||||
let penalty_score = 0;
|
||||
let time = 0;
|
||||
for (const j in journal) { if (tdoc.pids.includes(j.pid) && !(effective.includes(j.pid) && effective[j.pid].accept)) effective[j.pid] = j; }
|
||||
const _time = (jdoc) => jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000);
|
||||
function _penalty_score(jdoc) {
|
||||
const exceed_seconds = jdoc.rid.generationTime - Math.floor(tdoc.penalty_since / 1000);
|
||||
if (exceed_seconds < 0) return jdoc.score;
|
||||
return 0.5 * jdoc.score;
|
||||
for (const j in journal) {
|
||||
if (tdoc.pids.includes(j.pid)
|
||||
&& !(effective.includes(j.pid) && effective[j.pid].accept)) {
|
||||
effective[j.pid] = j;
|
||||
}
|
||||
}
|
||||
const _time = (jdoc) => jdoc.rid.generationTime - Math.floor(tdoc.beginAt / 1000);
|
||||
for (const j in effective) {
|
||||
detail.push({ ...effective[j], penalty_score: _penalty_score(effective[j]), time: _time(effective[j]) });
|
||||
detail.push({
|
||||
...effective[j],
|
||||
time: _time(effective[j]),
|
||||
});
|
||||
}
|
||||
for (const d of detail) {
|
||||
score += d.score;
|
||||
penalty_score += d.penalty_score;
|
||||
time += d.time;
|
||||
}
|
||||
return {
|
||||
score, penalty_score, time, detail,
|
||||
score, time, detail,
|
||||
};
|
||||
},
|
||||
scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, dudict, pdict) {
|
||||
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
|
||||
const columns = [
|
||||
{ type: 'rank', value: _('Rank') },
|
||||
{ type: 'user', value: _('User') },
|
||||
{ type: 'display_name', value: _('Display Name') },
|
||||
{ type: 'total_score', value: _('Score') },
|
||||
];
|
||||
if (is_export) {
|
||||
if (isExport) {
|
||||
columns.push(
|
||||
{ type: 'total_original_score', value: _('Original Score') },
|
||||
{ type: 'total_time', value: _('Total Time (Seconds)') },
|
||||
@@ -47,7 +42,7 @@ module.exports = {
|
||||
}
|
||||
columns.push({ type: 'total_time_str', value: _('Total Time') });
|
||||
for (const i in tdoc.pids) {
|
||||
if (is_export) {
|
||||
if (isExport) {
|
||||
columns.push(
|
||||
{ type: 'problem_score', value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title) },
|
||||
{ type: 'problem_original_score', value: '#{0} {1}'.format(i + 1, _('Original Score')) },
|
||||
@@ -57,16 +52,15 @@ module.exports = {
|
||||
} else columns.push({ type: 'problem_detail', value: '#{0}'.format(i + 1), raw: pdict[tdoc.pids[i]] });
|
||||
}
|
||||
const rows = [columns];
|
||||
for (const [rank, tsdoc] in ranked_tsdocs) {
|
||||
for (const [rank, tsdoc] in rankedTsdocs) {
|
||||
const tsddict = {};
|
||||
if (tsdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; }
|
||||
const row = [
|
||||
{ type: 'string', value: rank },
|
||||
{ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] },
|
||||
{ type: 'display_name', value: (dudict[tsdoc.uid] || {}).display_name || '' },
|
||||
{ type: 'string', value: tsdoc.penalty_score || 0 },
|
||||
];
|
||||
if (is_export) {
|
||||
if (isExport) {
|
||||
row.push(
|
||||
{ type: 'string', value: tsdoc.score || 0 },
|
||||
{ type: 'string', value: tsdoc.time || 0.0 },
|
||||
@@ -75,21 +69,21 @@ module.exports = {
|
||||
row.push({ type: 'string', value: tsdoc.time || 0 });
|
||||
for (const pid of tdoc.pids) {
|
||||
const rdoc = (tsddict[pid] || {}).rid || null;
|
||||
const col_score = (tsddict[pid] || {}).penalty_score || '-';
|
||||
const col_original_score = (tsddict[pid] || {}).score || '-';
|
||||
const col_time = (tsddict[pid] || {}).time || '-';
|
||||
const col_time_str = col_time != '-' ? col_time : '-';
|
||||
if (is_export) {
|
||||
const colScore = (tsddict[pid] || {}).penalty_score || '-';
|
||||
const colOriginalScore = (tsddict[pid] || {}).score || '-';
|
||||
const colTime = (tsddict[pid] || {}).time || '-';
|
||||
const colTimeStr = colTime !== '-' ? colTime : '-';
|
||||
if (isExport) {
|
||||
row.push(
|
||||
{ type: 'string', value: col_score },
|
||||
{ type: 'string', value: col_original_score },
|
||||
{ type: 'string', value: col_time },
|
||||
{ type: 'string', value: col_time_str },
|
||||
{ type: 'string', value: colScore },
|
||||
{ type: 'string', value: colOriginalScore },
|
||||
{ type: 'string', value: colTime },
|
||||
{ type: 'string', value: colTimeStr },
|
||||
);
|
||||
} else {
|
||||
row.push({
|
||||
type: 'record',
|
||||
value: '{0} / {1}\n{2}'.format(col_score, col_original_score, col_time_str),
|
||||
value: '{0} / {1}\n{2}'.format(colScore, colOriginalScore, colTimeStr),
|
||||
raw: rdoc,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,6 +115,13 @@ class Handler {
|
||||
this.response.redirect = this.request.headers.referer || '/';
|
||||
}
|
||||
|
||||
binary(data, name) {
|
||||
this.response.body = data;
|
||||
this.response.template = null;
|
||||
this.response.type = 'application/octet-stream';
|
||||
this.response.disposition = `attachment; filename="${name}"`;
|
||||
}
|
||||
|
||||
async ___prepare() {
|
||||
this.now = new Date();
|
||||
this._handler.sid = this.request.cookies.get('sid');
|
||||
@@ -170,6 +177,7 @@ class Handler {
|
||||
}
|
||||
|
||||
async putResponse() {
|
||||
if (this.response.disposition) this.ctx.set('Content-Disposition', this.response.disposition);
|
||||
if (this.response.redirect && !this.preferJson) {
|
||||
this.ctx.response.type = 'application/octet-stream';
|
||||
this.ctx.response.status = 302;
|
||||
|
||||
Reference in new issue
Block a user