Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
18 changes: 17 additions & 1 deletion packages/hydrooj/src/handler/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
import {
BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError,
InvalidTokenError, NotAssignedError, PermissionError, ValidationError,
InvalidTokenError, NotAssignedError, PermissionError, ProblemLockError,
ValidationError,
} from '../error';
import { ScoreboardConfig, Tdoc } from '../interface';
import paginate from '../lib/paginate';
Expand Down Expand Up @@ -640,6 +641,20 @@ export class ContestUserHandler extends ContestManagementBaseHandler {
this.back();
}
}

export class ContestProblemLockHandler extends Handler {
@param('tid', Types.ObjectId)
@param('pid', Types.UnsignedInt)
async post(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 +667,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);
}
14 changes: 11 additions & 3 deletions packages/hydrooj/src/handler/problem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
FileLimitExceededError, HackFailedError, NoProblemError, NotFoundError,
PermissionError, ProblemAlreadyExistError, ProblemAlreadyUsedByContestError, ProblemConfigError,
ProblemIsReferencedError, ProblemNotAllowLanguageError, ProblemNotAllowPretestError, ProblemNotFoundError,
ProblemIsReferencedError, ProblemLockError,
ProblemNotAllowLanguageError, ProblemNotAllowPretestError, ProblemNotFoundError,
RecordNotFoundError, SolutionNotFoundError, ValidationError,
} from '../error';
import {
Expand Down Expand Up @@ -479,7 +480,7 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
if (typeof this.pdoc.config === 'string') throw new ProblemConfigError();
}

async get() {
async get(domainId: string, tid?: ObjectId) {
this.response.template = 'problem_submit.html';
const langRange = (typeof this.pdoc.config === 'object' && this.pdoc.config.langs)
? Object.fromEntries(this.pdoc.config.langs.map((i) => [i, setting.langs[i]?.display || i]))
Expand All @@ -488,6 +489,7 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
langRange,
pdoc: this.pdoc,
udoc: this.udoc,
tdoc: this.tdoc,
title: this.pdoc.title,
page_name: this.tdoc
? this.tdoc.rule === 'homework'
Expand All @@ -503,6 +505,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 +578,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: 3 additions & 2 deletions packages/hydrooj/src/handler/record.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { pid } from 'process';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class

Unused import pid.
import {
omit, pick, throttle, uniqBy,
} from 'lodash';
Expand Down Expand Up @@ -58,7 +59,7 @@ 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 ? await problem.get(domainId, pid) : null)) {
throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
}
if (!(await contest.getStatus(domainId, tid, this.user._id))?.attend) {
Expand Down Expand Up @@ -152,7 +153,7 @@ class RecordDetailHandler extends ContestDetailBaseHandler {
} else if (rdoc.contest) {
this.tdoc = await contest.get(domainId, rdoc.contest);
let canView = this.user.own(this.tdoc);
canView ||= contest.canShowRecord.call(this, this.tdoc);
canView ||= contest.canShowRecord.call(this, this.tdoc, true, await problem.get(domainId, rdoc.pid));
canView ||= contest.canShowSelfRecord.call(this, this.tdoc, true) && rdoc.uid === this.user._id;
if (!canView && rdoc.uid !== this.user._id) throw new PermissionError(rid);
canViewDetail = canView;
Expand Down
5 changes: 4 additions & 1 deletion packages/hydrooj/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@ export interface Tdoc<docType = document['TYPE_CONTEST'] | document['TYPE_TRAINI
// For training
description?: string;
dag?: TrainingNode[];

// For codeforces
lockedList?: {};
}

export interface TrainingDoc extends Tdoc {
Expand Down Expand Up @@ -525,7 +528,7 @@ export interface ContestRule<T = any> {
submitAfterAccept: boolean;
showScoreboard: (tdoc: Tdoc<30>, now: Date) => boolean;
showSelfRecord: (tdoc: Tdoc<30>, now: Date) => boolean;
showRecord: (tdoc: Tdoc<30>, now: Date) => boolean;
showRecord: (tdoc: Tdoc<30>, now: Date, user?: User, pdoc?: ProblemDoc) => boolean;
stat: (this: ContestRule<T>, tdoc: Tdoc<30>, journal: any[]) => ContestStat & T;
scoreboardHeader: (
this: ContestRule<T>, config: ScoreboardConfig, _: (s: string) => string,
Expand Down
116 changes: 111 additions & 5 deletions packages/hydrooj/src/model/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import * as bus from '../service/bus';
import type { Handler } from '../service/server';
import { PERM, STATUS, STATUS_SHORT_TEXTS } from './builtin';
import * as document from './document';
import problem from './problem';
import problem, { ProblemDoc } from './problem';
import user, { User } from './user';

interface AcmJournal {
Expand Down Expand Up @@ -502,6 +502,98 @@ const ledo = buildContestRule({
},
}, oi);

const cf = buildContestRule({
TEXT: 'Codeforces',
check: () => { },
submitAfterAccept: false,
showScoreboard: (tdoc, now) => now > tdoc.beginAt,
showSelfRecord: () => true,
showRecord: (tdoc, now, user, pdoc) => {
if (now > tdoc.endAt) return true;
if (pdoc && tdoc.lockedList[pdoc.docId].includes(user._id)) return true;
return false;
},
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()) * (j.score * 100)) / (250 * 60000),
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 +744,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 +764,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 @@ -848,8 +940,8 @@ export function canViewHiddenScoreboard(this: { user: User }, tdoc: Tdoc<30>) {
return this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
}

export function canShowRecord(this: { user: User }, tdoc: Tdoc<30>, allowPermOverride = true) {
if (RULES[tdoc.rule].showRecord(tdoc, new Date())) return true;
export function canShowRecord(this: { user: User }, tdoc: Tdoc<30>, allowPermOverride = true, pdoc?: ProblemDoc) {
if (RULES[tdoc.rule].showRecord(tdoc, new Date(), this.user, pdoc)) return true;
if (allowPermOverride && canViewHiddenScoreboard.call(this, tdoc)) return true;
return false;
}
Expand Down Expand Up @@ -890,6 +982,18 @@ 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 +1026,6 @@ global.Hydro.model.contest = {
isLocked,
isExtended,
statusText,
getLockedList,
updateLockedList,
};
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadTool
</ToolbarButton>
)}
<ToolbarButton
disabled={this.props.isPosting || !!this.props.submitWaitSec}
disabled={this.props.isPosting || !!this.props.submitWaitSec ||
(UiContext.tdoc && UiContext.tdoc.lockedList[UiContext.pdoc.docId].includes(UserContext._id))}
className="scratchpad__toolbar__submit"
onClick={() => this.props.postSubmit(this.props)}
data-global-hotkey="f10"
Expand Down
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="POST">
<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
9 changes: 8 additions & 1 deletion packages/ui-default/templates/problem_submit.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{% extends "layout/basic.html" %}
{% block content %}
{{ set(UiContext, 'pdoc', pdoc) }}
{% if tdoc %}
{{ set(UiContext, 'tdoc', tdoc) }}
{% endif %}
<div class="row">
<div class="medium-9 columns">
<div class="section">
Expand All @@ -26,7 +29,11 @@ <h1 class="section__title">{{ _('Submit to Judge') }}</h1>
</div>
</div>
<div class="row"><div class="columns">
<input type="submit" class="rounded primary button" value="{{ _('Submit') }}">
{% if not tdoc or not tdoc.lockedList[pdoc.docId].includes(handler.user._id) %}
<input type="submit" class="rounded primary button" value="{{ _('Submit') }}">
{% else %}
<input disabled type="submit" class="rounded primary button" value="{{ _('Locked') }}">
{% endif %}
</div></div>
</form>
</div>
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