Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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) { // Maybe use method get was more convenient
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)) {
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))) {
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);
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
103 changes: 101 additions & 2 deletions packages/hydrooj/src/model/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,90 @@ 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 (STATUS.STATUS_ACCEPTED !== j.status) ntry[j.pid]++;
if ([STATUS.STATUS_HACK_SUCCESSFUL, STATUS.STATUS_HACK_UNSUCCESSFUL].includes(j.status)) {
if (j.status == STATUS.STATUS_HACK_SUCCESSFUL) detail[j.pid].hackSucc++;
else detail[j.pid].hackFail++;
continue;
}
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].penaltyScore -= 50 * detail[pid].hackFail;
detail[pid].penaltyScore += 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 +736,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 +756,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 +974,19 @@ export const statusText = (tdoc: Tdoc, tsdoc?: any) => (
? 'Live...'
: 'Done');

export async function getLockedList(domainId: string, tid: ObjectId) {
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 +1019,6 @@ global.Hydro.model.contest = {
isLocked,
isExtended,
statusText,
getLockedList,
updateLockedList,
};
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>
<form style="float: right; margin-left: 10px;" action="{{ url('contest_lock_problem', tid=tdoc.docId) }}" method="GET">
<input type="hidden" name="pid" value="{{ pid }}">
<button class="btn--lock" type="submit">
{% if not tdoc.lockedList[pid].includes(handler.user._id) %}
Lock
{% else %}
Locked
{% endif %}
</button>
</form>
{% 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