Skip to content

Commit 9b00205

Browse files
MolminLotuses-robot
andcommitted
init with hydro-dev#568
Co-Authored-By: Lotuses <87472564+lotuses-robot@users.noreply.github.com>
1 parent b7dc35d commit 9b00205

File tree

10 files changed

+172
-14
lines changed

10 files changed

+172
-14
lines changed

packages/hydrooj/src/error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export const ProblemAlreadyExistError = Err('ProblemAlreadyExistError', Forbidde
109109
export const ProblemAlreadyUsedByContestError = Err('ProblemAlreadyUsedByContestError', ForbiddenError, 'Problem {0} is already used by contest {1}.');
110110
export const ProblemNotAllowPretestError = Err('ProblemNotAllowPretestError', ForbiddenError, 'Pretesting is not supported for {0}.');
111111
export const ProblemNotAllowLanguageError = Err('ProblemNotAllowSubmitError', ForbiddenError, 'This language is not allowed to submit.');
112+
export const ProblemLockError = Err('ProblemLockError', ForbiddenError, 'Lock Error: {0}');
112113

113114
export const HackRejudgeFailedError = Err('HackRejudgeFailedError', BadRequestError, 'Cannot rejudge a hack record.');
114115
export const CannotDeleteSystemDomainError = Err('CannotDeleteSystemDomainError', BadRequestError, 'You are not allowed to delete system domain.');

packages/hydrooj/src/handler/contest.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { Context } from '../context';
1010
import {
1111
BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
1212
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError,
13-
InvalidTokenError, NotAssignedError, PermissionError, ValidationError,
13+
InvalidTokenError, NotAssignedError, PermissionError, ProblemLockError,
14+
ValidationError,
1415
} from '../error';
1516
import { ScoreboardConfig, Tdoc } from '../interface';
1617
import paginate from '../lib/paginate';
@@ -661,6 +662,19 @@ export class ContestManagementHandler extends ContestManagementBaseHandler {
661662
}
662663
}
663664

665+
export class ContestProblemLockHandler extends Handler {
666+
@param('tid', Types.ObjectId)
667+
@param('pid', Types.UnsignedInt)
668+
async post(domainId: string, tid: ObjectId, pid: number) {
669+
const lockList = await contest.getLockedList(domainId, tid);
670+
if (!lockList) throw new ProblemLockError('This contest is not lockable.');
671+
if (lockList[pid].includes(this.user._id)) throw new ProblemLockError('This problem has Locked before.');
672+
lockList[pid].push(this.user._id);
673+
await contest.updateLockedList(domainId, tid, lockList);
674+
this.back();
675+
}
676+
}
677+
664678
export class ContestFileDownloadHandler extends ContestDetailBaseHandler {
665679
@param('tid', Types.ObjectId)
666680
@param('filename', Types.Filename)
@@ -772,4 +786,5 @@ export async function apply(ctx: Context) {
772786
ctx.Route('contest_file_download', '/contest/:tid/file/:filename', ContestFileDownloadHandler, PERM.PERM_VIEW_CONTEST);
773787
ctx.Route('contest_user', '/contest/:tid/user', ContestUserHandler, PERM.PERM_VIEW_CONTEST);
774788
ctx.Route('contest_balloon', '/contest/:tid/balloon', ContestBalloonHandler, PERM.PERM_VIEW_CONTEST);
789+
ctx.Route('contest_lock_problem', '/contest/:tid/lock', ContestProblemLockHandler, PERM.PERM_VIEW_CONTEST);
775790
}

packages/hydrooj/src/handler/problem.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {
1111
BadRequestError, ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
1212
FileLimitExceededError, HackFailedError, NoProblemError, NotFoundError,
1313
PermissionError, ProblemAlreadyExistError, ProblemAlreadyUsedByContestError, ProblemConfigError,
14-
ProblemIsReferencedError, ProblemNotAllowLanguageError, ProblemNotAllowPretestError, ProblemNotFoundError,
15-
RecordNotFoundError, SolutionNotFoundError, ValidationError,
14+
ProblemIsReferencedError, ProblemLockError, ProblemNotAllowLanguageError, ProblemNotAllowPretestError,
15+
ProblemNotFoundError, RecordNotFoundError, SolutionNotFoundError, ValidationError,
1616
} from '../error';
1717
import {
1818
ProblemDoc, ProblemSearchOptions, ProblemStatusDoc, RecordDoc, User,
@@ -494,7 +494,7 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
494494
if (this.pdoc.config.langs && !this.pdoc.config.langs.length) throw new ProblemConfigError();
495495
}
496496

497-
async get() {
497+
async get(domainId: string, tid?: ObjectId) {
498498
this.response.template = 'problem_submit.html';
499499
const langRange = (typeof this.pdoc.config === 'object' && this.pdoc.config.langs)
500500
? Object.fromEntries(this.pdoc.config.langs.map((i) => [i, setting.langs[i]?.display || i]))
@@ -520,6 +520,12 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
520520
@param('input', Types.String, true)
521521
@param('tid', Types.ObjectId, true)
522522
async post(domainId: string, lang: string, code: string, pretest = false, input = '', tid?: ObjectId) {
523+
if (tid) {
524+
const tdoc = await contest.get(domainId, tid);
525+
if (tdoc.rule === 'cf' && tdoc.lockedList[this.pdoc.docId].includes(this.user._id)) {
526+
throw new ProblemLockError('You have locked this problem.');
527+
}
528+
}
523529
const config = this.pdoc.config;
524530
if (typeof config === 'string' || config === null) throw new ProblemConfigError();
525531
if (['submit_answer', 'objective'].includes(config.type)) {
@@ -586,7 +592,7 @@ export class ProblemHackHandler extends ProblemDetailHandler {
586592
if (!this.rdoc || this.rdoc.pid !== this.pdoc.docId
587593
|| this.rdoc.contest?.toString() !== tid?.toString()) throw new RecordNotFoundError(domainId, rid);
588594
if (tid) {
589-
if (this.tdoc.rule !== 'codeforces') throw new HackFailedError('This contest is not hackable.');
595+
if (this.tdoc.rule !== 'cf') throw new HackFailedError('This contest is not hackable.');
590596
if (!contest.isOngoing(this.tdoc, this.tsdoc)) throw new ContestNotLiveError(this.tdoc.docId);
591597
}
592598
if (this.rdoc.uid === this.user._id) throw new HackFailedError('You cannot hack your own submission');

packages/hydrooj/src/handler/record.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class RecordListHandler extends ContestDetailBaseHandler {
5959
this.tdoc = tdoc;
6060
if (!tdoc) throw new ContestNotFoundError(domainId, pid);
6161
if (!contest.canShowScoreboard.call(this, tdoc, true)) throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
62-
if (!contest[q.uid === this.user._id ? 'canShowSelfRecord' : 'canShowRecord'].call(this, tdoc, true)) {
62+
if (!contest[q.uid === this.user._id ? 'canShowSelfRecord' : 'canShowRecord'].call(this, tdoc, true, pid ? await problem.get(domainId, pid) : null)) {
6363
throw new PermissionError(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
6464
}
6565
if (!(await contest.getStatus(domainId, tid, this.user._id))?.attend) {
@@ -157,7 +157,7 @@ class RecordDetailHandler extends ContestDetailBaseHandler {
157157
} else if (rdoc.contest) {
158158
this.tdoc = await contest.get(domainId, rdoc.contest);
159159
let canView = this.user.own(this.tdoc);
160-
canView ||= contest.canShowRecord.call(this, this.tdoc);
160+
canView ||= contest.canShowRecord.call(this, this.tdoc, true, await problem.get(domainId, rdoc.pid));
161161
canView ||= contest.canShowSelfRecord.call(this, this.tdoc, true) && rdoc.uid === this.user._id;
162162
if (!canView && rdoc.uid !== this.user._id) throw new PermissionError(rid);
163163
canViewDetail = canView;

packages/hydrooj/src/interface.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,9 @@ export interface Tdoc extends Document {
408408
// For training
409409
description?: string;
410410
dag?: TrainingNode[];
411+
412+
// For codeforces
413+
lockedList?: {};
411414
}
412415

413416
export interface TrainingDoc extends Omit<Tdoc, 'docType'> {
@@ -539,7 +542,7 @@ export interface ContestRule<T = any> {
539542
submitAfterAccept: boolean;
540543
showScoreboard: (tdoc: Tdoc, now: Date) => boolean;
541544
showSelfRecord: (tdoc: Tdoc, now: Date) => boolean;
542-
showRecord: (tdoc: Tdoc, now: Date) => boolean;
545+
showRecord: (tdoc: Tdoc, now: Date, user?: User, pdoc?: ProblemDoc) => boolean;
543546
stat: (this: ContestRule<T>, tdoc: Tdoc, journal: any[]) => ContestStat & T;
544547
scoreboardHeader: (
545548
this: ContestRule<T>, config: ScoreboardConfig, _: (s: string) => string,

packages/hydrooj/src/model/contest.ts

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type { Handler } from '../service/server';
1616
import { Optional } from '../typeutils';
1717
import { PERM, STATUS, STATUS_SHORT_TEXTS } from './builtin';
1818
import * as document from './document';
19-
import problem from './problem';
19+
import problem, { ProblemDoc } from './problem';
2020
import user, { User } from './user';
2121

2222
interface AcmJournal {
@@ -569,6 +569,98 @@ const ledo = buildContestRule({
569569
},
570570
}, oi);
571571

572+
const cf = buildContestRule({
573+
TEXT: 'Codeforces',
574+
check: () => { },
575+
submitAfterAccept: false,
576+
showScoreboard: (tdoc, now) => now > tdoc.beginAt,
577+
showSelfRecord: () => true,
578+
showRecord: (tdoc, now, user, pdoc) => {
579+
if (now > tdoc.endAt) return true;
580+
if (pdoc && tdoc.lockedList[pdoc.docId].includes(user._id)) return true;
581+
return false;
582+
},
583+
stat(tdoc, journal) {
584+
const ntry = Counter<number>();
585+
const hackSucc = Counter<number>();
586+
const hackFail = Counter<number>();
587+
const detail = {};
588+
for (const j of journal) {
589+
if ([STATUS.STATUS_COMPILE_ERROR, STATUS.STATUS_FORMAT_ERROR].includes(j.status)) continue;
590+
// if (this.submitAfterAccept) continue;
591+
if (STATUS.STATUS_ACCEPTED !== j.status) ntry[j.pid]++;
592+
if ([STATUS.STATUS_HACK_SUCCESSFUL, STATUS.STATUS_HACK_UNSUCCESSFUL].includes(j.status)) {
593+
if (j.status === STATUS.STATUS_HACK_SUCCESSFUL) detail[j.pid].hackSucc++;
594+
else detail[j.pid].hackFail++;
595+
continue;
596+
}
597+
const timePenaltyScore = Math.round(Math.max(j.score * 100
598+
- ((j.rid.getTimestamp().getTime() - tdoc.beginAt.getTime()) * (j.score * 100)) / (250 * 60000),
599+
j.score * 100 * 0.3));
600+
const penaltyScore = Math.max(timePenaltyScore - 50 * (ntry[j.pid]), 0);
601+
if (!detail[j.pid] || detail[j.pid].penaltyScore < penaltyScore) {
602+
detail[j.pid] = {
603+
...j,
604+
penaltyScore,
605+
timePenaltyScore,
606+
ntry: ntry[j.pid],
607+
hackFail: hackFail[j.pid],
608+
hackSucc: hackSucc[j.pid],
609+
};
610+
}
611+
}
612+
let score = 0;
613+
let originalScore = 0;
614+
for (const pid of tdoc.pids) {
615+
if (!detail[pid]) continue;
616+
detail[pid].penaltyScore -= 50 * detail[pid].hackFail;
617+
detail[pid].penaltyScore += 100 * detail[pid].hackSucc;
618+
score += detail[pid].penaltyScore;
619+
originalScore += detail[pid].score;
620+
}
621+
return {
622+
score, originalScore, detail,
623+
};
624+
},
625+
async scoreboardRow(config, _, tdoc, pdict, udoc, rank, tsdoc, meta) {
626+
const tsddict = tsdoc.detail || {};
627+
const row: ScoreboardRow = [
628+
{ type: 'rank', value: rank.toString() },
629+
{ type: 'user', value: udoc.uname, raw: tsdoc.uid },
630+
];
631+
if (config.isExport) {
632+
row.push({ type: 'email', value: udoc.mail });
633+
row.push({ type: 'string', value: udoc.school || '' });
634+
row.push({ type: 'string', value: udoc.displayName || '' });
635+
row.push({ type: 'string', value: udoc.studentId || '' });
636+
}
637+
row.push({
638+
type: 'total_score',
639+
value: tsdoc.score || 0,
640+
hover: tsdoc.score !== tsdoc.originalScore ? _('Original score: {0}').format(tsdoc.originalScore) : '',
641+
});
642+
for (const s of tsdoc.journal || []) {
643+
if (!pdict[s.pid]) continue;
644+
pdict[s.pid].nSubmit++;
645+
if (s.status === STATUS.STATUS_ACCEPTED) pdict[s.pid].nAccept++;
646+
}
647+
for (const pid of tdoc.pids) {
648+
row.push({
649+
type: 'record',
650+
value: tsddict[pid]?.penaltyScore || '',
651+
hover: tsddict[pid]?.penaltyScore
652+
? `${tsddict[pid].timePenaltyScore}, -${tsddict[pid].ntry}, +${tsddict[pid].hackSucc} , -${tsddict[pid].hackFail}`
653+
: '',
654+
raw: tsddict[pid]?.rid,
655+
style: tsddict[pid]?.status === STATUS.STATUS_ACCEPTED && tsddict[pid]?.rid.getTimestamp().getTime() === meta?.first?.[pid]
656+
? 'background-color: rgb(217, 240, 199);'
657+
: undefined,
658+
});
659+
}
660+
return row;
661+
},
662+
}, oi);
663+
572664
const homework = buildContestRule({
573665
TEXT: 'Assignment',
574666
hidden: true,
@@ -719,7 +811,7 @@ const homework = buildContestRule({
719811
});
720812

721813
export const RULES: ContestRules = {
722-
acm, oi, homework, ioi, ledo, strictioi,
814+
acm, oi, homework, ioi, ledo, strictioi, cf,
723815
};
724816

725817
const collBalloon = db.collection('contest.balloon');
@@ -741,7 +833,7 @@ export async function add(
741833
RULES[rule].check(data);
742834
await bus.parallel('contest/before-add', data);
743835
const res = await document.add(domainId, content, owner, document.TYPE_CONTEST, null, null, null, {
744-
...data, title, rule, beginAt, endAt, pids, attend: 0, rated,
836+
...data, title, rule, beginAt, endAt, pids, attend: 0, rated, lockedList: pids.reduce((acc, curr) => ({ ...acc, [curr]: [] }), {}),
745837
});
746838
await bus.parallel('contest/add', data, res);
747839
return res;
@@ -906,8 +998,8 @@ export function canViewHiddenScoreboard(this: { user: User }, tdoc: Tdoc) {
906998
return this.user.hasPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
907999
}
9081000

909-
export function canShowRecord(this: { user: User }, tdoc: Tdoc, allowPermOverride = true) {
910-
if (RULES[tdoc.rule].showRecord(tdoc, new Date())) return true;
1001+
export function canShowRecord(this: { user: User }, tdoc: Tdoc, allowPermOverride = true, pdoc?: ProblemDoc) {
1002+
if (RULES[tdoc.rule].showRecord(tdoc, new Date(), this.user, pdoc)) return true;
9111003
if (allowPermOverride && canViewHiddenScoreboard.call(this, tdoc)) return true;
9121004
return false;
9131005
}
@@ -984,6 +1076,18 @@ export const statusText = (tdoc: Tdoc, tsdoc?: any) => (
9841076
? 'Live...'
9851077
: 'Done');
9861078

1079+
export async function getLockedList(domainId: string, tid: ObjectId) {
1080+
const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid);
1081+
if (tdoc.rule !== 'cf') return null;
1082+
return tdoc.lockedList;
1083+
}
1084+
1085+
export async function updateLockedList(domainId: string, tid: ObjectId, $lockList: any) {
1086+
const tdoc = await document.get(domainId, document.TYPE_CONTEST, tid);
1087+
tdoc.lockedList = $lockList;
1088+
edit(domainId, tid, tdoc);
1089+
}
1090+
9871091
global.Hydro.model.contest = {
9881092
RULES,
9891093
buildContestRule,
@@ -1026,4 +1130,6 @@ global.Hydro.model.contest = {
10261130
isExtended,
10271131
applyProjection,
10281132
statusText,
1133+
getLockedList,
1134+
updateLockedList,
10291135
};

packages/ui-default/components/scratchpad/ScratchpadToolbarContainer.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadTool
125125
</ToolbarButton>
126126
)}
127127
<ToolbarButton
128-
disabled={this.props.isPosting || !!this.props.submitWaitSec}
128+
disabled={this.props.isPosting || !!this.props.submitWaitSec ||
129+
(UiContext.tdoc && UiContext.tdoc.lockedList[UiContext.pdoc.docId].includes(UserContext._id))}
129130
className="scratchpad__toolbar__submit"
130131
onClick={() => this.props.postSubmit(this.props)}
131132
data-global-hotkey="f10"

packages/ui-default/templates/contest_problemlist.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ <h1 class="section__title">{{ _('Problems') }}</h1>
5050
<a href="{{ url('problem_detail', pid=pid, query={tid:tdoc.docId}) }}">
5151
<b>{{ String.fromCharCode(65+loop.index0) }}</b>&nbsp;&nbsp;{{ pdict[pid].title }}
5252
</a>
53+
{% if tdoc.rule === "cf" %}
54+
<a style="float: right; margin-left: 10px;" href="{{ url('record_main', query={tid:tdoc.docId, pid:pid, status:1}) }}" target="_blank">Hack</a>
55+
<form style="float: right; margin-left: 10px;" action="{{ url('contest_lock_problem', tid=tdoc.docId) }}" method="POST">
56+
<input type="hidden" name="pid" value="{{ pid }}">
57+
<button class="btn--lock" type="submit">
58+
{% if not tdoc.lockedList[pid].includes(handler.user._id) %}
59+
Lock
60+
{% else %}
61+
Locked
62+
{% endif %}
63+
</button>
64+
</form>
65+
{% endif %}
5366
</td>
5467
</tr>
5568
{%- endfor -%}

packages/ui-default/templates/problem_submit.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{% extends "layout/basic.html" %}
22
{% block content %}
33
{{ set(UiContext, 'pdoc', pdoc) }}
4+
{% if tdoc %}
5+
{{ set(UiContext, 'tdoc', tdoc) }}
6+
{% endif %}
47
<div class="row">
58
<div class="medium-9 columns">
69
<div class="section">
@@ -26,7 +29,11 @@ <h1 class="section__title">{{ _('Submit to Judge') }}</h1>
2629
</div>
2730
</div>
2831
<div class="row"><div class="columns">
32+
{% if not tdoc or not tdoc.lockedList[pdoc.docId].includes(handler.user._id) %}
2933
<input type="submit" class="rounded primary button" value="{{ _('Submit') }}">
34+
{% else %}
35+
<input disabled type="submit" class="rounded primary button" value="{{ _('Locked') }}">
36+
{% endif %}
3037
</div></div>
3138
</form>
3239
</div>

packages/ui-default/templates/record_detail.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,15 @@ <h1 class="section__title">{{ _('Information') }}</h1>
5252
<div class="section__body no-padding">
5353
<ol class="menu">
5454
<li class="menu__item">
55+
{% if rdoc.contest %}
56+
<a class="menu__link" href="{{ url('problem_hack', pid=pdoc.pid|default(pdoc.docId), rid=rdoc._id, query={tid:rdoc.contest}) }}">
57+
<span class="icon icon-debug"></span> {{ _('Hack') }}
58+
</a>
59+
{% else %}
5560
<a class="menu__link" href="{{ url('problem_hack', pid=pdoc.pid|default(pdoc.docId), rid=rdoc._id) }}">
5661
<span class="icon icon-debug"></span> {{ _('Hack') }}
5762
</a>
63+
{% endif %}
5864
</li>
5965
</ol>
6066
</div>

0 commit comments

Comments
 (0)