Skip to content

Commit 069d204

Browse files
committed
feat: practice contests
1 parent 531d32f commit 069d204

File tree

26 files changed

+241
-62
lines changed

26 files changed

+241
-62
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
Warnings:
3+
4+
- Made the column `public` on table `competitions` required. This step will fail if there are existing NULL values in that column.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE `competitions` ADD COLUMN `practice` BOOLEAN NOT NULL DEFAULT false,
9+
MODIFY `public` BOOLEAN NOT NULL DEFAULT false;

backend/prisma/schema.prisma

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ model competitions {
1717
deleted_at DateTime? @db.DateTime(0)
1818
updated_at DateTime @db.DateTime(0)
1919
rating Int @default(0)
20-
public Boolean?
20+
public Boolean @default(false)
21+
practice Boolean @default(false)
2122
scheduled_at DateTime? @db.DateTime(0)
2223
scheduled_end_at DateTime? @db.DateTime(0)
2324
questions questions[]

backend/src/api/routes/competition.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import config from '../../config/config';
1212
var router = express.Router();
1313
const client = Container.get(DatabaseProvider).client();
1414

15+
// Delete a contest
1516
router.delete('/competition/:id', authenticate, loginRequired, (req, res) => {
1617
client.competitions
1718
.update({
@@ -35,10 +36,12 @@ router.delete('/competition/:id', authenticate, loginRequired, (req, res) => {
3536
});
3637
});
3738

39+
// Create a contest
3840
router.post('/competition', authenticate, loginRequired, (req, res) => {
3941
const title = req.body.title;
42+
const practice = Boolean(req.body.practice);
4043

41-
if (title == null || (title as string).length > 20) {
44+
if (title == null || (title as string).length > 120) {
4245
Util.sendResponse(
4346
res,
4447
resCode.badRequest,
@@ -53,6 +56,7 @@ router.post('/competition', authenticate, loginRequired, (req, res) => {
5356
title: req.body.title,
5457
description: req.body.description,
5558
public: false,
59+
practice: practice,
5660
created_at: new Date(),
5761
host_user_id: res.locals.user.id,
5862
updated_at: new Date(),
@@ -70,6 +74,7 @@ router.post('/competition', authenticate, loginRequired, (req, res) => {
7074
});
7175
});
7276

77+
// Update a contest
7378
router.put('/competition', authenticate, loginRequired, (req, res) => {
7479
const competitionBody = req.body;
7580
if (
@@ -123,6 +128,7 @@ router.put('/competition', authenticate, loginRequired, (req, res) => {
123128
});
124129
});
125130

131+
// Fetch a contest
126132
router.get('/competition/:id', authenticate, loginRequired, (req, res) => {
127133
if (req.params.id == '') {
128134
Util.sendResponse(res, resCode.badRequest);
@@ -175,6 +181,7 @@ function isAnyErrorPresent(obj: any) {
175181
return error;
176182
}
177183

184+
// Fetch quality of a contest
178185
router.get(
179186
'/competition/:id/quality',
180187
authenticate,
@@ -279,6 +286,7 @@ router.get(
279286
}
280287

281288
if (
289+
question.question_choices?.length &&
282290
question.question_choices.every((q) => q.is_correct)
283291
) {
284292
questionWarnings.question_choices.push(
@@ -327,6 +335,7 @@ router.get(
327335
}
328336
);
329337

338+
// Fetch all contests
330339
router.get('/competitions', authenticate, (req, res) => {
331340
const user: UserInfo | null = res.locals.user;
332341
const params = {

backend/src/api/routes/judge.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function safeRouteToQuestion(
6868
);
6969
return;
7070
}
71-
resolve(question);
71+
resolve(question as QuestionInfo);
7272
});
7373
});
7474

@@ -181,9 +181,23 @@ function judgeMcqQuestion(req: Request, res: Response) {
181181
user_id: user.id,
182182
question_id: question.id,
183183
},
184+
include: {
185+
question: {
186+
include: {
187+
competitions: {
188+
select: {
189+
practice: true,
190+
},
191+
},
192+
},
193+
},
194+
},
184195
})
185196
.then((existingResult) => {
186-
if (existingResult) {
197+
if (
198+
existingResult &&
199+
!existingResult.question.competitions.practice
200+
) {
187201
Util.sendResponse(
188202
res,
189203
resCode.badRequest,
@@ -257,9 +271,23 @@ function judgeFillQuestion(req: Request, res: Response) {
257271
user_id: user.id,
258272
question_id: question.id,
259273
},
274+
include: {
275+
question: {
276+
include: {
277+
competitions: {
278+
select: {
279+
practice: true,
280+
},
281+
},
282+
},
283+
},
284+
},
260285
})
261286
.then((existingResult) => {
262-
if (existingResult) {
287+
if (
288+
existingResult &&
289+
!existingResult.question.competitions.practice
290+
) {
263291
Util.sendResponse(
264292
res,
265293
resCode.badRequest,
@@ -324,9 +352,20 @@ function judgeLongQuestion(req: Request, res: Response) {
324352
user_id: user.id,
325353
question_id: question.id,
326354
},
355+
include: {
356+
question: {
357+
include: {
358+
competitions: {
359+
select: {
360+
practice: true,
361+
},
362+
},
363+
},
364+
},
365+
},
327366
})
328367
.then((existingResult) => {
329-
if (existingResult) {
368+
if (existingResult && !question.competitions!.practice) {
330369
Util.sendResponse(
331370
res,
332371
resCode.badRequest,
@@ -344,7 +383,8 @@ function judgeLongQuestion(req: Request, res: Response) {
344383
output: '',
345384
success: success,
346385
},
347-
question
386+
question,
387+
question.competitions!.practice
348388
);
349389

350390
Util.sendResponse(res, resCode.success);

backend/src/api/routes/question.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,9 @@ router.post(
117117
}
118118

119119
judgeService
120-
.execute(execReq, false, null, null)
120+
.execute(execReq, true, null, null)
121121
.then((exeRes) => {
122+
console.log(exeRes);
122123
client.question_verification
123124
.upsert({
124125
where: {
@@ -640,6 +641,13 @@ router.get(
640641
question_choices: true,
641642
},
642643
},
644+
host_user: {
645+
select: {
646+
id: true,
647+
name: true,
648+
avatar_url: true,
649+
},
650+
},
643651
},
644652
})
645653
.then((competition) => {

backend/src/api/routes/user.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ async function getUserStats(user_id: number) {
5959
deleted_at: null,
6060
competitions: {
6161
deleted_at: null,
62+
practice: false,
6263
},
6364
},
6465
user_id: user_id,

backend/src/config/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export type QuestionInfo = {
2828
sol_code_file?: boolean;
2929
user_answer?: string;
3030
question_choices?: QuestionChoice[];
31+
competitions?: CompetitionInfo;
3132
};
3233

3334
export type QuestionChoice = {
@@ -84,6 +85,7 @@ export type CompetitionInfo = {
8485
scheduled_end_at: Date;
8586
rating: number;
8687
public: boolean;
88+
practice: boolean;
8789
};
8890

8991
export type CodeSolution = {

backend/src/services/scoreboardService.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export class ScoreboardService {
1717
user: UserInfo,
1818
hunterExecutable: HunterExecutable,
1919
exeInfo: ExeInfo,
20-
question: QuestionInfo
20+
question: QuestionInfo,
21+
practice = false
2122
) {
2223
var points = -1 * question.neg_points;
2324

@@ -97,6 +98,11 @@ export class ScoreboardService {
9798
evaluated_at: null,
9899
result: 0,
99100
};
101+
102+
// We won't require manual evaluation for practice contests.
103+
if (practice) {
104+
result_data.evaluated_at = new Date();
105+
}
100106
break;
101107
default:
102108
throw 'Question type not supported';

src/app/common/components/competitions-list/competitions-list.component.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
<div id="c_title" [ngClass]="{ 'untitled': !com.title }">
5656
{{ com.title || "untitled" }}
5757
</div>
58+
<div *ngIf="!showAuthor" class="info-like">
59+
Created
60+
{{ com.created_at | timeAgo }}
61+
</div>
5862
<div
5963
id="c_host_user"
6064
routerLink="/u/{{ com.host_user_id }}"
@@ -88,8 +92,13 @@
8892
[icon]="privateIcon"
8993
style="color: crimson"
9094
></fa-icon>
95+
&nbsp;
96+
<span style="color: crimson"> Private </span>
9197
</span>
9298
</div>
99+
<div class="detail-tag cute-panel" *ngIf="com.practice">
100+
For Practice Only
101+
</div>
93102
<div
94103
id="start_schedule"
95104
class="detail-tag"

src/app/common/components/create-dialog/create-dialog.component.html

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,27 @@
1717
maxlength="120"
1818
placeholder="Example: Winter Challenge Wave-II"
1919
></textarea>
20-
<br />
2120
<p class="info-like">
2221
You will be redirected to the Hunter Editor. Be sure to read the Guide
2322
to create the best competition. Keep in mind that any poorly designed
2423
Public competition can be reported by the users.
2524
</p>
25+
26+
<p>
27+
Make it a practice contest? (Cannot be changed later) &nbsp;&nbsp;
28+
<mat-slide-toggle [(ngModel)]="practiceContest"></mat-slide-toggle>
29+
</p>
30+
<p class="info-like">
31+
Enable practice mode and it will allow participants to attempt the
32+
contest multiple times to practice. The points earned from a practice
33+
contest won't be added into a user's profile, and will only be effective
34+
in this contest's scoreboard.
35+
</p>
36+
2637
<button id="create" class="main" (click)="requestCreateCompetition()">
2738
<fa-icon [icon]="cubeIcon"></fa-icon> &nbsp; Initialize a new
28-
competition
39+
{{ practiceContest ? "practice" : "competitive" }}
40+
contest
2941
</button>
3042
<div class="error">{{ responseMessage }}</div>
3143
</popup>

0 commit comments

Comments
 (0)