core: contest: export as ghost

This commit is contained in:
undefined committed 2022-11-15 18:47:24 +08:00
1 parent 6ddaa8cf0c
commit 9da8321677
2 files changed
+53 -10

No files matched your search

+47 -3
View File
@@ -3,20 +3,22 @@ import { statSync } from 'fs-extra';
import { pick } from 'lodash';
import moment from 'moment-timezone';
import { ObjectID } from 'mongodb';
import { sortFiles, Time } from '@hydrooj/utils/lib/utils';
import { Counter, sortFiles, Time } from '@hydrooj/utils/lib/utils';
import {
BadRequestError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
ContestScoreboardHiddenError,
ForbiddenError, InvalidTokenError, PermissionError,
ValidationError,
} from '../error';
import { Tdoc } from '../interface';
import paginate from '../lib/paginate';
import { PERM, PRIV } from '../model/builtin';
import { PERM, PRIV, STATUS } from '../model/builtin';
import * as contest from '../model/contest';
import message from '../model/message';
import * as oplog from '../model/oplog';
import problem from '../model/problem';
import record from '../model/record';
import RecordModel from '../model/record';
import ScheduleModel from '../model/schedule';
import storage from '../model/storage';
import * as system from '../model/system';
@@ -230,7 +232,7 @@ export class ContestBroadcastHandler extends ContestDetailBaseHandler {
export class ContestScoreboardHandler extends ContestDetailBaseHandler {
@param('tid', Types.ObjectID)
@param('ext', Types.Range(['csv', 'html']), true)
@param('ext', Types.Range(['csv', 'html', 'ghost']), true)
async get(domainId: string, tid: ObjectID, ext = '') {
if (ext) {
await this.exportScoreboard(domainId, tid, ext);
@@ -247,8 +249,50 @@ export class ContestScoreboardHandler extends ContestDetailBaseHandler {
};
}
async exportGhost(domainId: string, tid: ObjectID) {
const tdoc = await contest.get(domainId, tid);
if (!contest.canShowScoreboard.call(this, tdoc)) throw new ContestScoreboardHiddenError(tid);
const [pdict, teams] = await Promise.all([
problem.getList(domainId, tdoc.pids, true, undefined, false, undefined, true),
contest.getMultiStatus(domainId, { docId: tid }).toArray(),
]);
const udict = await user.getList(domainId, teams.map((i) => i.uid));
const teamIds: Record<number, number> = {};
for (let i = 1; i <= teams.length; i++) teamIds[teams[i - 1].uid] = i;
const time = (t: ObjectID) => Math.floor((t.getTimestamp().getTime() - tdoc.beginAt.getTime()) / Time.second);
const pid = (i: number) => String.fromCharCode(65 + i);
const escape = (i: string) => i.replace(/[",]/g, '');
const unknownSchool = this.translate('Unknown School');
const submissions = teams.flatMap((i, idx) => {
if (!i.journal) return [];
const journal = i.journal.filter((s) => tdoc.pids.includes(s.pid));
const c = Counter();
return journal.map((s) => {
const id = pid(tdoc.pids.indexOf(s.pid));
c[id]++;
return `@s ${idx + 1},${id},${c[id]},${time(s.rid)},${s.status === STATUS.STATUS_ACCEPTED ? 'AC' : 'RJ'}`;
});
});
const res = [
`@contest "${escape(tdoc.title)}"`,
`@contlen ${Math.floor((tdoc.endAt.getTime() - tdoc.beginAt.getTime()) / Time.minute)}`,
`@problems ${tdoc.pids.length}`,
`@teams ${tdoc.attend}`,
`@submissions ${submissions.length}`,
].concat(
tdoc.pids.map((i, idx) => `@p ${pid(idx)},${escape(pdict[i].title)},20,0`),
teams.map((i, idx) => `@t ${idx + 1},0,1,${escape(udict[i.uid].school || unknownSchool)}-${escape(udict[i.uid].uname)}`),
submissions,
);
this.binary(res.join('\n'), `${this.tdoc.title}.ghost`);
}
async exportScoreboard(domainId: string, tid: ObjectID, ext: string) {
await this.limitRate('scoreboard_download', 120, 3);
if (ext === 'ghost') {
await this.exportGhost(domainId, tid);
return;
}
const getContent = {
csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value?.toString().replace(/\n/g, ' ')).join(','))).join('\n')}`,
html: (rows, tdoc) => this.renderHTML('contest_scoreboard_download_html.html', { rows, tdoc }),
@@ -5,7 +5,7 @@
{% macro renderRecord(cell, canView) %}
<a href="{{ url('record_detail', rid=cell.raw) if canView else '' }}"{% if cell.hover %} data-tooltip="{{ cell.hover }}"{% endif %}>
{%- if cell.status == status.STATUS_JUDGING -%}
{%- if cell.status == STATUS.STATUS_WAITING -%}
<span style="font-weight:bold;color:yellow">?</span>
{%- else -%}
{%- set _color = utils.status.getScoreColor(cell.score|default(cell.value)) -%}
@@ -18,12 +18,11 @@
<div class="row"><div class="medium-12 columns scoreboard--{{ tdoc.rule }}">
<div class="section visible">
<div class="section__header">
<a class="button" target="_blank" href="{{ url(type + '_scoreboard_download', tid=tdoc.docId, ext='html') }}">
<span class="icon icon-download"></span> {{ _('Export as HTML') }}
</a>
<a class="button" href="{{ url(type + '_scoreboard_download', tid=tdoc.docId, ext='csv') }}">
<span class="icon icon-download"></span> {{ _('Export as CSV') }}
</a>
{%- for ext in ['HTML','CSV','Ghost'] -%}
<a class="button" target="_blank" href="{{ url(type + '_scoreboard_download', tid=tdoc.docId, ext=ext.toLowerCase()) }}">
<span class="icon icon-download"></span> {{ _('Export as '+ext) }}
</a>
{%- endfor -%}
{% if model.contest.isDone(tdoc) and tdoc.lockAt and handler.user.own(tdoc) and not tdoc.unlocked %}
<form method="POST">
<input type="hidden" name="operation" value="unlock">