Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core&ui: Add Codeforces contest mode #568

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/hydrooj/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const ProblemAlreadyExistError = Err('ProblemAlreadyExistError', Forbidde
export const ProblemAlreadyUsedByContestError = Err('ProblemAlreadyUsedByContestError', ForbiddenError, 'Problem {0} is already used by contest {1}.');
export const ProblemNotAllowPretestError = Err('ProblemNotAllowPretestError', ForbiddenError, 'This {0} is not allow run pretest.');
export const ProblemNotAllowLanguageError = Err('ProblemNotAllowSubmitError', ForbiddenError, 'This language is not allow to submit.');
export const ProblemLockError = Err('ProblemLockError', ForbiddenError, 'Lock Error: {0}');

export const HackRejudgeFailedError = Err('HackRejudgeFailedError', BadRequestError, 'Cannot rejudge a hack record.');
export const CannotDeleteSystemDomainError = Err('CannotDeleteSystemDomainError', BadRequestError, 'You are not allowed to delete system domain.');
Expand Down
17 changes: 16 additions & 1 deletion packages/hydrooj/src/handler/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import {
BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError,
InvalidTokenError, NotAssignedError, PermissionError, ValidationError,
InvalidTokenError, NotAssignedError, PermissionError, ValidationError, ProblemLockError,
} from '../error';
import { ScoreboardConfig, Tdoc } from '../interface';
import paginate from '../lib/paginate';
Expand Down Expand Up @@ -640,6 +640,20 @@ export class ContestUserHandler extends ContestManagementBaseHandler {
this.back();
}
}

export class ContestProblemLockHandler extends Handler {
@param('tid', Types.ObjectId)
@param('pid', Types.UnsignedInt)
async get(domainId: string, tid: ObjectId, pid: number) {
const lockList = await contest.getLockedList(domainId, tid);
if (!lockList) throw new ProblemLockError('This contest is not lockable.');
if (lockList[pid].includes(this.user._id)) throw new ProblemLockError('This problem has Locked before.');
lockList[pid].push(this.user._id);
await contest.updateLockedList(domainId, tid, lockList);
this.back();
}
}

export async function apply(ctx) {
ctx.Route('contest_create', '/contest/create', ContestEditHandler);
ctx.Route('contest_main', '/contest', ContestListHandler, PERM.PERM_VIEW_CONTEST);
Expand All @@ -652,4 +666,5 @@ export async function apply(ctx) {
ctx.Route('contest_code', '/contest/:tid/code', ContestCodeHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_file_download', '/contest/:tid/file/:filename', ContestFileDownloadHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_user', '/contest/:tid/user', ContestUserHandler, PERM.PERM_VIEW_CONTEST);
ctx.Route('contest_lock_problem', '/contest/:tid/lock', ContestProblemLockHandler, PERM.PERM_VIEW_CONTEST);
}
10 changes: 8 additions & 2 deletions packages/hydrooj/src/handler/problem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
FileLimitExceededError, HackFailedError, NoProblemError, NotFoundError,
PermissionError, ProblemAlreadyExistError, ProblemAlreadyUsedByContestError, ProblemConfigError,
ProblemIsReferencedError, ProblemNotAllowLanguageError, ProblemNotAllowPretestError, ProblemNotFoundError,
RecordNotFoundError, SolutionNotFoundError, ValidationError,
RecordNotFoundError, SolutionNotFoundError, ValidationError, ProblemLockError,
} from '../error';
import {
ProblemDoc, ProblemSearchOptions, ProblemStatusDoc, RecordDoc, User,
Expand Down Expand Up @@ -503,6 +503,12 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
@param('input', Types.String, true)
@param('tid', Types.ObjectId, true)
async post(domainId: string, lang: string, code: string, pretest = false, input = '', tid?: ObjectId) {
if (tid) {
const tdoc = await contest.get(domainId, tid);
if (tdoc.rule === 'cf' && tdoc.lockedList[this.pdoc.docId].includes(this.user._id)) {
Lotuses-robot marked this conversation as resolved.
Show resolved Hide resolved
throw new ProblemLockError("You have locked this problem.");
}
}
const config = this.pdoc.config;
if (typeof config === 'string' || config === null) throw new ProblemConfigError();
if (['submit_answer', 'objective'].includes(config.type)) {
Expand Down Expand Up @@ -570,7 +576,7 @@ export class ProblemHackHandler extends ProblemDetailHandler {
if (!this.rdoc || this.rdoc.pid !== this.pdoc.docId
|| this.rdoc.contest?.toString() !== tid?.toString()) throw new RecordNotFoundError(domainId, rid);
if (tid) {
if (this.tdoc.rule !== 'codeforces') throw new HackFailedError('This contest is not hackable.');
if (this.tdoc.rule !== 'cf') throw new HackFailedError('This contest is not hackable.');
if (!contest.isOngoing(this.tdoc, this.tsdoc)) throw new ContestNotLiveError(this.tdoc.docId);
}
if (this.rdoc.uid === this.user._id) throw new HackFailedError('You cannot hack your own submission');
Expand Down
5 changes: 4 additions & 1 deletion packages/hydrooj/src/handler/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ class RecordListHandler extends ContestDetailBaseHandler {
this.tdoc = tdoc;
if (!tdoc) throw new ContestNotFoundError(domainId, pid);
if (!contest.canShowScoreboard.call(this, tdoc, true)) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
if (!contest[q.uid === this.user._id ? 'canShowSelfRecord' : 'canShowRecord'].call(this, tdoc, true)) {
if ((!contest[q.uid === this.user._id ? 'canShowSelfRecord' : 'canShowRecord'].call(this, tdoc, true))
&& (!pid || tdoc.rule === "cf" && !tdoc.lockedList[parseInt(pid.toString())].includes(this.user._id))) {
Lotuses-robot marked this conversation as resolved.
Show resolved Hide resolved
throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
}
if (!(await contest.getStatus(domainId, tid, this.user._id))?.attend) {
Expand Down Expand Up @@ -154,6 +155,7 @@ class RecordDetailHandler extends ContestDetailBaseHandler {
let canView = this.user.own(this.tdoc);
canView ||= contest.canShowRecord.call(this, this.tdoc);
canView ||= contest.canShowSelfRecord.call(this, this.tdoc, true) && rdoc.uid === this.user._id;
canView ||= this.tdoc.rule === "cf" && this.tdoc.lockedList[parseInt(rdoc.pid.toString())].includes(this.user._id);
Lotuses-robot marked this conversation as resolved.
Show resolved Hide resolved
if (!canView && rdoc.uid !== this.user._id) throw new PermissionError(rid);
canViewDetail = canView;
this.args.tid = this.tdoc.docId;
Expand All @@ -170,6 +172,7 @@ class RecordDetailHandler extends ContestDetailBaseHandler {
canViewCode ||= this.user.hasPriv(PRIV.PRIV_READ_RECORD_CODE);
canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE);
canViewCode ||= this.user.hasPerm(PERM.PERM_READ_RECORD_CODE_ACCEPT) && self?.status === STATUS.STATUS_ACCEPTED;
canViewCode ||= this.tdoc.rule === "cf" && this.tdoc.lockedList[parseInt(rdoc.pid.toString())].includes(this.user._id);
if (this.tdoc) {
const tsdoc = await contest.getStatus(domainId, this.tdoc.docId, this.user._id);
if (this.tdoc.allowViewCode && contest.isDone(this.tdoc)) {
Expand Down
100 changes: 98 additions & 2 deletions packages/hydrooj/src/model/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,87 @@ const ledo = buildContestRule({
},
}, oi);

const cf = buildContestRule({
TEXT: 'Codeforces',
check: () => { },
submitAfterAccept: false,
showScoreboard: (tdoc, now) => now > tdoc.beginAt,
showSelfRecord: () => true,
showRecord: (tdoc, now) => now > tdoc.endAt,
stat(tdoc, journal) {
const ntry = Counter<number>();
const hackSucc = Counter<number>();
const hackFail = Counter<number>();
const detail = {};
for (const j of journal) {
if ([STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_FORMAT_ERROR].includes(j.status)) continue;
// if (this.submitAfterAccept) continue;
if (j.status === STATUS.STATUS_HACK_SUCCESSFUL) hackSucc[j.pid]++;
if (j.status === STATUS.STATUS_HACK_UNSUCCESSFUL) hackFail[j.pid]++;
if (STATUS.STATUS_ACCEPTED !== j.status) ntry[j.pid]++;
const timePenaltyScore = Math.round(Math.max(j.score * 100 - (j.rid.getTimestamp().getTime() - tdoc.beginAt.getTime()) / 1000 / 60 * j.score * 100 / 250, j.score * 100 * 0.3));
const penaltyScore = Math.max(timePenaltyScore - 50 * (ntry[j.pid]), 0);
if (!detail[j.pid] || detail[j.pid].penaltyScore < penaltyScore) {
detail[j.pid] = {
...j,
penaltyScore,
timePenaltyScore,
ntry: ntry[j.pid],
hackFail: hackFail[j.pid],
hackSucc: hackSucc[j.pid],
};
}
}
let score = 0;
let originalScore = 0;
for (const pid of tdoc.pids) {
if (!detail[pid]) continue;
detail[pid].score -= 50 * detail[pid].hackFail;
detail[pid].score += 100 * detail[pid].hackSucc;
score += detail[pid].penaltyScore;
originalScore += detail[pid].score;
}
return {
score, originalScore, detail,
};
},
async scoreboardRow(config, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
const tsddict = tsdoc.detail || {};
const row: ScoreboardRow = [
{ type: 'rank', value: rank.toString() },
{ type: 'user', value: udoc.uname, raw: tsdoc.uid },
];
if (config.isExport) {
row.push({ type: 'email', value: udoc.mail });
row.push({ type: 'string', value: udoc.school || '' });
row.push({ type: 'string', value: udoc.displayName || '' });
row.push({ type: 'string', value: udoc.studentId || '' });
}
row.push({
type: 'total_score',
value: tsdoc.score || 0,
hover: tsdoc.score !== tsdoc.originalScore ? _('Original score: {0}').format(tsdoc.originalScore) : '',
});
for (const s of tsdoc.journal || []) {
if (!pdict[s.pid]) continue;
pdict[s.pid].nSubmit++;
if (s.status === STATUS.STATUS_ACCEPTED) pdict[s.pid].nAccept++;
}
for (const pid of tdoc.pids) {
row.push({
type: 'record',
value: tsddict[pid]?.penaltyScore || '',
hover: tsddict[pid]?.penaltyScore ? `${tsddict[pid].timePenaltyScore}, -${tsddict[pid].ntry}, +${tsddict[pid].hackSucc} , -${tsddict[pid].hackFail}` : '',
raw: tsddict[pid]?.rid,
style: tsddict[pid]?.status === STATUS.STATUS_ACCEPTED && tsddict[pid]?.rid.getTimestamp().getTime() === meta?.first?.[pid]
? 'background-color: rgb(217, 240, 199);'
: undefined,
});
}
return row;
},
}, oi);

const homework = buildContestRule({
TEXT: 'Assignment',
hidden: true,
Expand Down Expand Up @@ -652,7 +733,7 @@ const homework = buildContestRule({
});

export const RULES: ContestRules = {
acm, oi, homework, ioi, ledo, strictioi,
acm, oi, homework, ioi, ledo, strictioi, cf,
};

function _getStatusJournal(tsdoc) {
Expand All @@ -672,7 +753,7 @@ export async function add(
RULES[rule].check(data);
await bus.parallel('contest/before-add', data);
const res = await document.add(domainId, content, owner, document.TYPE_CONTEST, null, null, null, {
...data, title, rule, beginAt, endAt, pids, attend: 0, rated,
...data, title, rule, beginAt, endAt, pids, attend: 0, rated, lockedList: pids.reduce((acc, curr) => ({ ...acc, [curr]: [] }), {}),
});
await bus.parallel('contest/add', data, res);
return res;
Expand Down Expand Up @@ -890,6 +971,19 @@ export const statusText = (tdoc: Tdoc, tsdoc?: any) => (
? 'Live...'
: 'Done');

export async function getLockedList(domainId: string, tid: ObjectId) {
Lotuses-robot marked this conversation as resolved.
Show resolved Hide resolved
const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid);
if (tdoc.rule !== 'cf') return null;
return tdoc.lockedList;
}

export async function updateLockedList(domainId: string, tid: ObjectId, $lockList: any) {
const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid);
tdoc.lockedList = $lockList;
edit(domainId, tid, tdoc);
}


global.Hydro.model.contest = {
RULES,
add,
Expand Down Expand Up @@ -922,4 +1016,6 @@ global.Hydro.model.contest = {
isLocked,
isExtended,
statusText,
getLockedList,
updateLockedList,
};
10 changes: 10 additions & 0 deletions packages/ui-default/components/contest/contest.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ const contestPage = new AutoloadPage('contestPage', () => {
Notification.error(e.message || e);
});
});
$('.btn--lock').on('click', (ev) => {
Lotuses-robot marked this conversation as resolved.
Show resolved Hide resolved
console.log('fk');
const id = $(this).attr('id');
request.get(`./lock?pid=${id}`).then(() => {
Notification.success(i18n('Successfully locked'));
delay(1000).then(() => window.location.reload());
}).catch((e) => {
Notification.error(e.message || e);
})
});
});

export default contestPage;
13 changes: 13 additions & 0 deletions packages/ui-default/templates/contest_problemlist.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ <h1 class="section__title">{{ _('Problems') }}</h1>
<a href="{{ url('problem_detail', pid=pid, query={tid:tdoc.docId}) }}">
<b>{{ String.fromCharCode(65+loop.index0) }}</b>&nbsp;&nbsp;{{ pdict[pid].title }}
</a>
{% if tdoc.rule === "cf" %}
<a style="float: right; margin-left: 10px;" href="{{ url('record_main', query={tid:tdoc.docId, pid:pid, status:1}) }}" target="_blank">Hack</a>
{% if not tdoc.lockedList[pid].includes(handler.user._id) %}
<form style="float: right; margin-left: 10px;" action="{{ url('contest_lock_problem', tid=tdoc.docId) }}" method="GET">
Lotuses-robot marked this conversation as resolved.
Show resolved Hide resolved
<input type="hidden" name="pid" value="{{ pid }}">
<button class="btn--lock" type="submit">
Lock
</button>
</form>
{% else %}
<a style="float: right; margin-left: 10px;" target="_blank">Locked</a>
{% endif %}
{% endif %}
</td>
</tr>
{%- endfor -%}
Expand Down
12 changes: 9 additions & 3 deletions packages/ui-default/templates/record_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,15 @@ <h1 class="section__title">{{ _('Information') }}</h1>
<div class="section__body no-padding">
<ol class="menu">
<li class="menu__item">
<a class="menu__link" href="{{ url('problem_hack', pid=pdoc.pid|default(pdoc.docId), rid=rdoc._id) }}">
<span class="icon icon-debug"></span> {{ _('Hack') }}
</a>
{% if rdoc.contest %}
<a class="menu__link" href="{{ url('problem_hack', pid=pdoc.pid|default(pdoc.docId), rid=rdoc._id, query={tid:rdoc.contest}) }}">
<span class="icon icon-debug"></span> {{ _('Hack') }}
</a>
{% else %}
<a class="menu__link" href="{{ url('problem_hack', pid=pdoc.pid|default(pdoc.docId), rid=rdoc._id) }}">
<span class="icon icon-debug"></span> {{ _('Hack') }}
</a>
{% endif %}
</li>
</ol>
</div>
Expand Down