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

feat(be): calculate contest participants score #1643

Merged
merged 45 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c46f38b
fix(be): add finish time field on contest record
gyunseo Apr 8, 2024
0f2b055
feat(be): add finish time
gyunseo Apr 8, 2024
7fe60a5
feat(be): implement the calculation of contest participants score
gyunseo Apr 8, 2024
42394af
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo Apr 8, 2024
7678890
fix(be): change seed script for test
gyunseo Apr 8, 2024
649e73b
fix(be): add accepted problem num increment logic
gyunseo Apr 8, 2024
09b84bb
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo May 6, 2024
efceb60
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo May 9, 2024
6c64635
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo May 13, 2024
b3a8f1d
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo Jul 1, 2024
8526a92
fix(be): fix conflict exception message
gyunseo Jul 1, 2024
26b30f3
fix(be): refactor logic using early return
gyunseo Jul 1, 2024
d14a856
fix(be): add comment to explain the fix
gyunseo Jul 1, 2024
1801296
test(be): add bruno api docs
gyunseo Jul 1, 2024
56323f9
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo Jul 3, 2024
0ca856f
fix(be): resolve client submission test error
gyunseo Jul 4, 2024
dade6f8
Merge branch 'main' into 1238-calculate-contest-participants-score
cho-to Jul 4, 2024
b007c89
fix(be): add to be equal
gyunseo Jul 4, 2024
d67faee
Merge branch 'hotfix-resolve-client-submission-test-error' into 1238-…
gyunseo Jul 4, 2024
bb7340f
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo Jul 4, 2024
e340b62
fix(be): add promise resolve value of submission update stub
gyunseo Jul 4, 2024
5140c14
test(be): add a test scenario for creating submission with constest id
gyunseo Jul 4, 2024
6caeeb8
test(be): add test scenario for creating submission with workbook id
gyunseo Jul 4, 2024
b2edbe0
test(be): add test scenario for creating submission for already AC pr…
gyunseo Jul 4, 2024
a9d5bc4
test(be): add scenario for updating submission result with conest id
gyunseo Jul 4, 2024
33142af
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo Jul 7, 2024
6eeb716
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo Jul 8, 2024
ef84d12
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo Jul 11, 2024
c512436
chore(be): add comments on schema
gyunseo Jul 11, 2024
c148594
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo Jul 11, 2024
b0e48d2
feat(be): add contest not found handling logic
gyunseo Jul 11, 2024
08654de
docs(be): add no contest found bru doc
gyunseo Jul 11, 2024
b38edd4
docs(be): add create submission for contest with accpeted code burno doc
gyunseo Jul 11, 2024
6635bff
docs(be): add create submission for contest with wrong answer code br…
gyunseo Jul 11, 2024
8c195dd
docs(be): add create submission for contest with time limit exceeded
gyunseo Jul 11, 2024
2ce213d
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo Jul 15, 2024
56afdfb
test(be): add contest find or throw mocking data
gyunseo Jul 15, 2024
c21a186
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo Jul 15, 2024
88d0751
test(be): add enable copy paste field to resolve type check error
gyunseo Jul 15, 2024
0362acd
docs(be): fix bruno api docs description and assertion
gyunseo Jul 15, 2024
4ed6e31
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo Jul 15, 2024
675068e
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo Jul 18, 2024
3011a7d
test(be): add invitation code to contest mock data
gyunseo Jul 18, 2024
75b1ac0
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo Jul 22, 2024
0321c4d
Merge branch 'main' into 1238-calculate-contest-participants-score
gyunseo Jul 22, 2024
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
93 changes: 89 additions & 4 deletions apps/backend/apps/client/src/submission/submission.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { NotFoundException } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { Test, type TestingModule } from '@nestjs/testing'
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'
import { Language, ResultStatus, Role, type User } from '@prisma/client'
import {
Language,
ResultStatus,
Role,
type Contest,
type User
} from '@prisma/client'
import { expect } from 'chai'
import { plainToInstance } from 'class-transformer'
import { TraceService } from 'nestjs-otel'
Expand Down Expand Up @@ -38,6 +44,9 @@ const db = {
findUnique: stub(),
update: stub()
},
contest: {
findFirstOrThrow: stub()
},
contestProblem: {
findUniqueOrThrow: stub(),
findFirstOrThrow: stub()
Expand All @@ -46,7 +55,8 @@ const db = {
findUniqueOrThrow: stub()
},
contestRecord: {
findUniqueOrThrow: stub()
findUniqueOrThrow: stub(),
update: stub()
},
user: {
findFirstOrThrow: stub(),
Expand All @@ -57,6 +67,21 @@ const db = {

const CONTEST_ID = 1
const WORKBOOK_ID = 1
const mockContest: Contest = {
id: CONTEST_ID,
createdById: 1,
groupId: 1,
title: 'SKKU Coding Platform 모의대회',
description: 'test',
invitationCode: 'test',
startTime: new Date(),
endTime: new Date(),
isVisible: true,
isRankVisible: true,
enableCopyPaste: true,
createTime: new Date(),
updateTime: new Date()
}

describe('SubmissionService', () => {
let service: SubmissionService
Expand Down Expand Up @@ -121,6 +146,7 @@ describe('SubmissionService', () => {
describe('submitToContest', () => {
it('should call createSubmission', async () => {
const createSpy = stub(service, 'createSubmission')
db.contest.findFirstOrThrow(mockContest)
db.contestRecord.findUniqueOrThrow.resolves({
contest: {
groupId: 1,
Expand Down Expand Up @@ -216,6 +242,60 @@ describe('SubmissionService', () => {
expect(publishSpy.calledOnce).to.be.true
})

it('should create submission with contestId', async () => {
const publishSpy = stub(amqpConnection, 'publish')
db.problem.findUnique.resolves(problems[0])
db.submission.create.resolves({
...submissions[0],
contestId: CONTEST_ID
})
db.submission.findMany.resolves(submissions)
expect(
await service.createSubmission(
submissionDto,
problems[0],
submissions[0].userId,
{ contestId: CONTEST_ID }
)
).to.be.deep.equal({ ...submissions[0], contestId: CONTEST_ID })
expect(publishSpy.calledOnce).to.be.true
})

it('should throw conflict found exception if user has already gotten AC', async () => {
const publishSpy = stub(amqpConnection, 'publish')
db.problem.findUnique.resolves(problems[0])
db.submission.create.resolves(submissions[0])
db.submission.findMany.resolves([{ result: ResultStatus.Accepted }])

await expect(
service.createSubmission(
submissionDto,
problems[0],
submissions[0].userId,
{ contestId: CONTEST_ID }
)
).to.be.rejectedWith(ConflictFoundException)
expect(publishSpy.calledOnce).to.be.false
})

it('should create submission with workbookId', async () => {
const publishSpy = stub(amqpConnection, 'publish')
db.problem.findUnique.resolves(problems[0])
db.submission.create.resolves({
...submissions[0],
workbookId: WORKBOOK_ID
})
expect(
await service.createSubmission(
submissionDto,
problems[0],
submissions[0].userId,
{ workbookId: WORKBOOK_ID }
)
).to.be.deep.equal({ ...submissions[0], workbookId: WORKBOOK_ID })
expect(publishSpy.calledOnce).to.be.true
})

it('should throw exception if the language is not supported', async () => {
const publishSpy = stub(amqpConnection, 'publish')
db.problem.findUnique.resolves(problems[0])
Expand Down Expand Up @@ -280,19 +360,24 @@ describe('SubmissionService', () => {
]
}
}

db.submission.update.resolves(submissions[0])
await expect(service.handleJudgerMessage(target)).to.be.rejectedWith(
UnprocessableDataException
)
db.submission.update.reset()
})
})

describe('updateSubmissionResult', () => {
it('should call update submission result', async () => {
db.submission.update.reset()
db.submission.update.resolves(submissions[0])
db.submission.update.resolves({
...submissions[0],
contestId: CONTEST_ID
})
db.problem.findFirstOrThrow.resolves(problems[0])
db.problem.update.reset()

submissionResults.forEach((result, index) => {
db.submissionResult.create.onCall(index).resolves(result)
})
Expand Down
119 changes: 105 additions & 14 deletions apps/backend/apps/client/src/submission/submission.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,18 @@ export class SubmissionService implements OnModuleInit {
groupId = OPEN_SPACE_ID
) {
const now = new Date()

await this.prisma.contest.findFirstOrThrow({
where: {
id: contestId,
groupId,
startTime: {
lte: now
},
endTime: {
gt: now
}
}
})
const { contest } = await this.prisma.contestRecord.findUniqueOrThrow({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
Expand Down Expand Up @@ -192,8 +203,6 @@ export class SubmissionService implements OnModuleInit {
userId: number,
idOptions?: { contestId?: number; workbookId?: number }
) {
let submission: Submission

if (!problem.languages.includes(submissionDto.language)) {
throw new ConflictFoundException(
`This problem does not support language ${submissionDto.language}`
Expand All @@ -219,23 +228,45 @@ export class SubmissionService implements OnModuleInit {
...data
}

if (idOptions && idOptions.contestId) {
submission = await this.prisma.submission.create({
data: { ...submissionData, contestId: idOptions.contestId }
// idOptions Object가 undefined이거나 contestId와 workbookId가 모두 없는 경우
if (
idOptions === undefined ||
(!idOptions.contestId && !idOptions.workbookId)
) {
const submission = await this.prisma.submission.create({
data: submissionData
})
} else if (idOptions && idOptions.workbookId) {
submission = await this.prisma.submission.create({
data: { ...submissionData, workbookId: idOptions.workbookId }
await this.publishJudgeRequestMessage(code, submission)
return submission
}

if (idOptions.contestId) {
// 해당 contestId에 해당하는 Contest에서 해당 problemId에 해당하는 문제로 AC를 받은 submission이 있는지 확인
const hasPassed = await this.hasPassedProblem(userId, {
problemId: problem.id,
contestId: idOptions.contestId
})
} else {
submission = await this.prisma.submission.create({
data: submissionData
if (hasPassed) {
throw new ConflictFoundException(
'You have already gotten AC for this problem'
)
}
const submission = await this.prisma.submission.create({
data: { ...submissionData, contestId: idOptions.contestId }
})

await this.publishJudgeRequestMessage(code, submission)
return submission
}

await this.publishJudgeRequestMessage(code, submission)
if (idOptions.workbookId) {
const submission = await this.prisma.submission.create({
data: { ...submissionData, workbookId: idOptions.workbookId }
})

return submission
await this.publishJudgeRequestMessage(code, submission)
return submission
}
}

isValidCode(code: Snippet[], language: Language, templates: Template[]) {
Expand Down Expand Up @@ -439,6 +470,66 @@ export class SubmissionService implements OnModuleInit {
}
})
}

// contestId가 있는 경우에는 contestRecord 업데이트
// participants들의 score와 penalty를 업데이트
if (submission.userId && submission.contestId) {
const contestId = submission.contestId
const userId = submission.userId
let toBeAddedScore = 0,
toBeAddedPenalty = 0,
toBeAddedAcceptedProblemNum = 0,
isFinishTimeToBeUpdated = false
const contestRecord = await this.prisma.contestRecord.findUniqueOrThrow({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
contestId_userId: {
contestId,
userId
}
},
select: {
id: true,
acceptedProblemNum: true,
score: true,
totalPenalty: true,
finishTime: true
}
})

if (resultStatus === ResultStatus.Accepted) {
toBeAddedScore = (
await this.prisma.contestProblem.findFirstOrThrow({
where: {
contestId,
problemId: submission.problemId
},
select: {
score: true
}
})
).score
isFinishTimeToBeUpdated = true
toBeAddedAcceptedProblemNum = 1
} else {
toBeAddedPenalty = 1
}

await this.prisma.contestRecord.update({
where: {
id: contestRecord.id
},
data: {
acceptedProblemNum:
contestRecord.acceptedProblemNum + toBeAddedAcceptedProblemNum,
score: contestRecord.score + toBeAddedScore,
totalPenalty: contestRecord.totalPenalty + toBeAddedPenalty,
finishTime: isFinishTimeToBeUpdated
? submission.updateTime
: contestRecord.finishTime
}
})
}
}

// FIXME: Workbook 구분
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "contest_record" ADD COLUMN "finish_time" TIMESTAMP(3);
24 changes: 14 additions & 10 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ model ContestProblem {
contestId Int @map("contest_id")
problem Problem @relation(fields: [problemId], references: [id], onDelete: Cascade)
problemId Int @map("problem_id")
// 각 문제의 점수 (비율 아님)
score Int @default(0)
gyunseo marked this conversation as resolved.
Show resolved Hide resolved
createTime DateTime @default(now()) @map("create_time")
updateTime DateTime @updatedAt @map("update_time")
Expand All @@ -321,16 +322,19 @@ model Announcement {
}

model ContestRecord {
id Int @id @default(autoincrement())
contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade)
contestId Int @map("contest_id")
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
userId Int? @map("user_id")
acceptedProblemNum Int @default(0) @map("accepted_problem_num")
score Int @default(0)
totalPenalty Int @default(0) @map("total_penalty")
createTime DateTime @default(now()) @map("create_time")
updateTime DateTime @updatedAt @map("update_time")
id Int @id @default(autoincrement())
contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade)
contestId Int @map("contest_id")
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
userId Int? @map("user_id")
acceptedProblemNum Int @default(0) @map("accepted_problem_num")
score Int @default(0)
// finishTime: Pariticipant가 가장 최근에 AC를 받은 시각
finishTime DateTime? @map("finish_time")
cho-to marked this conversation as resolved.
Show resolved Hide resolved
// totalPenalty: Submission 시, AC를 받지 못했을 때, 올라가는 Counter
totalPenalty Int @default(0) @map("total_penalty")
gyunseo marked this conversation as resolved.
Show resolved Hide resolved
createTime DateTime @default(now()) @map("create_time")
updateTime DateTime @updatedAt @map("update_time")

@@unique([contestId, userId])
@@map("contest_record")
Expand Down
9 changes: 4 additions & 5 deletions apps/backend/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1301,7 +1301,8 @@ const createContests = async () => {
data: {
order: problem.id - 1,
contestId: ongoingContests[0].id,
problemId: problem.id
problemId: problem.id,
score: problem.id * 10
}
})
}
Expand Down Expand Up @@ -1700,7 +1701,6 @@ const createCodeDrafts = async () => {

const createContestRecords = async () => {
const contestRecords: ContestRecord[] = []
let i = 0
// group 1 users
const group1Users = await prisma.userGroup.findMany({
where: {
Expand All @@ -1712,12 +1712,11 @@ const createContestRecords = async () => {
data: {
userId: user.userId,
contestId: 1,
acceptedProblemNum: user.userId,
totalPenalty: i * 60
acceptedProblemNum: 0,
totalPenalty: 0
}
})
contestRecords.push(contestRecord)
i++
}

// upcoming contest에 참가한 User 1의 contest register를 un-register하는 기능과,
Expand Down
Loading
Loading