Skip to content

Commit

Permalink
feat(be): add get-contest-submission-informations api (#1894)
Browse files Browse the repository at this point in the history
* feat(be): add contest-submission-result model

* chore(be): rename contest-submission-result to contest-submission-information

* chore(be): set nullability of fields in contest-submission-information

* feat(be): add get-contest-submission-informations api

* chore(be): rename

* test(be): add test

* docs(be): add docs

* chore(be): rename files and add fields on contest-submission-summary-for-one

* feat(be): implement combine score summary and submissions

* chore(be): comment test

* feat(be): add problem-id option

* fix(be): fix wrong type

* docs(be): rename files

* docs(be): rename file

* docs(be): rename files

* fix(be): fix ContestSubmissionSummaryForOne model

* docs(be): remove resolved todo

* chore(be): lint contest.service.spec

* fix(be): don't throw error when no submission

---------

Co-authored-by: 강민석 <[email protected]>
Co-authored-by: jimin9038 <[email protected]>
  • Loading branch information
3 people authored and jwoojin9 committed Aug 28, 2024
1 parent 5afcd9d commit 9910124
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 6 deletions.
37 changes: 36 additions & 1 deletion apps/backend/apps/admin/src/contest/contest.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ import {
EntityNotExistException,
UnprocessableDataException
} from '@libs/exception'
import { CursorValidationPipe, GroupIDPipe, RequiredIntPipe } from '@libs/pipe'
import {
CursorValidationPipe,
GroupIDPipe,
IDValidationPipe,
RequiredIntPipe
} from '@libs/pipe'
import { ContestService } from './contest.service'
import { ContestSubmissionSummaryForUser } from './model/contest-submission-summary-for-user.model'
import { ContestWithParticipants } from './model/contest-with-participants.model'
import { CreateContestInput } from './model/contest.input'
import { UpdateContestInput } from './model/contest.input'
Expand Down Expand Up @@ -224,6 +230,35 @@ export class ContestResolver {
}
}

@Query(() => ContestSubmissionSummaryForUser)
async getContestSubmissionSummaryByUserId(
@Args('contestId', { type: () => Int }, IDValidationPipe) contestId: number,
@Args('userId', { type: () => Int }, IDValidationPipe) userId: number,
@Args('problemId', { nullable: true, type: () => Int }, IDValidationPipe)
problemId: number,
@Args(
'take',
{ type: () => Int, defaultValue: 10 },
new RequiredIntPipe('take')
)
take: number,
@Args('cursor', { nullable: true, type: () => Int }, CursorValidationPipe)
cursor: number | null
) {
try {
return await this.contestService.getContestSubmissionSummaryByUserId(
take,
contestId,
userId,
problemId,
cursor
)
} catch (error) {
this.logger.error(error)
throw new InternalServerErrorException()
}
}

@Mutation(() => DuplicatedContestResponse)
async duplicateContest(
@Args('groupId', { type: () => Int }, GroupIDPipe) groupId: number,
Expand Down
75 changes: 75 additions & 0 deletions apps/backend/apps/admin/src/contest/contest.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ContestProblem, Group, ContestRecord } from '@generated'
import { Problem } from '@generated'
import { Contest } from '@generated'
import { faker } from '@faker-js/faker'
import { ResultStatus } from '@prisma/client'
import type { Cache } from 'cache-manager'
import { expect } from 'chai'
import { stub } from 'sinon'
Expand Down Expand Up @@ -141,6 +142,41 @@ const contestProblem: ContestProblem = {
updateTime: faker.date.past()
}

const submissionsWithProblemTitleAndUsername = {
id: 1,
userId: 1,
userIp: '127.0.0.1',
problemId: 1,
contestId: 1,
workbookId: 1,
code: [],
codeSize: 1,
language: 'C',
result: ResultStatus.Accepted,
createTime: '2000-01-01',
updateTime: '2000-01-02',
problem: {
title: 'submission'
},
user: {
username: 'user01',
studentId: '1234567890'
}
}

// const submissionResults = [
// {
// id: 1,
// submissionId: 1,
// problemTestcaseId: 1,
// result: ResultStatus.Accepted,
// cpuTime: BigInt(1),
// memory: 1,
// createTime: '2000-01-01',
// updateTime: '2000-01-02'
// }
// ]

const publicizingRequest: PublicizingRequest = {
contestId,
userId,
Expand Down Expand Up @@ -181,6 +217,7 @@ const db = {
contestProblem: {
create: stub().resolves(ContestProblem),
findMany: stub().resolves([ContestProblem]),
findFirstOrThrow: stub().resolves(ContestProblem),
findFirst: stub().resolves(ContestProblem)
},
contestRecord: {
Expand All @@ -195,6 +232,12 @@ const db = {
group: {
findUnique: stub().resolves(Group)
},
submission: {
findMany: stub().resolves([submissionsWithProblemTitleAndUsername])
},
// submissionResult: {
// findMany: stub().resolves([submissionResults])
// },
$transaction: stub().callsFake(async () => {
const updatedProblem = await db.problem.update()
const newContestProblem = await db.contestProblem.create()
Expand Down Expand Up @@ -358,6 +401,38 @@ describe('ContestService', () => {
})
})

// describe('getContestSubmissionSummaryByUserId', () => {
// it('should return contest submission summaries', async () => {
// const res = await service.getContestSubmissionSummaryByUserId(10, 1, 1, 1)

// expect(res.submissions).to.deep.equal([
// {
// contestId: 1,
// problemTitle: 'submission',
// username: 'user01',
// studentId: '1234567890',
// submissionResult: ResultStatus.Accepted,
// language: 'C',
// submissionTime: '2000-01-01',
// codeSize: 1,
// ip: '127.0.0.1' // TODO: submission.ip 사용
// }
// ])
// expect(res.scoreSummary).to.deep.equal({
// totalProblemCount: 1,
// submittedProblemCount: 1,
// totalScore: 1,
// acceptedTestcaseCountPerProblem: [
// {
// acceptedTestcaseCount: 0,
// problemId: 1,
// totalTestcaseCount: 1
// }
// ]
// })
// })
// })

// describe('duplicateContest', () => {
// db['$transaction'] = stub().callsFake(async () => {
// const newContest = await db.contest.create()
Expand Down
71 changes: 66 additions & 5 deletions apps/backend/apps/admin/src/contest/contest.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,59 @@ export class ContestService {
return contestProblems
}

async getContestSubmissionSummaryByUserId(
take: number,
contestId: number,
userId: number,
problemId: number | null,
cursor: number | null
) {
const paginator = this.prisma.getPaginator(cursor)
const submissions = await this.prisma.submission.findMany({
...paginator,
take,
where: {
userId,
contestId,
problemId: problemId ?? undefined
},
include: {
problem: {
select: {
title: true
}
},
user: {
select: {
username: true,
studentId: true
}
}
}
})

const mappedSubmission = submissions.map((submission) => {
return {
contestId: submission.contestId,
problemTitle: submission.problem.title,
username: submission.user?.username,
studentId: submission.user?.studentId,
submissionResult: submission.result,
language: submission.language,
submissionTime: submission.createTime,
codeSize: submission.codeSize,
ip: submission.userIp
}
})

const scoreSummary = await this.getContestScoreSummary(userId, contestId)

return {
scoreSummary,
submissions: mappedSubmission
}
}

/**
* Duplicate contest with contest problems and users who participated in the contest
* Not copied: submission
Expand Down Expand Up @@ -608,15 +661,23 @@ export class ContestService {
}
})
])

if (!submissions.length) {
throw new EntityNotExistException('Submissions')
} else if (!contestProblems.length) {
if (!contestProblems.length) {
throw new EntityNotExistException('ContestProblems')
} else if (!contestRecord) {
throw new EntityNotExistException('contestRecord')
}

if (!submissions.length) {
return {
submittedProblemCount: 0,
totalProblemCount: contestProblems.length,
userContestScore: 0,
contestPerfectScore: contestProblems.reduce(
(total, { score }) => total + score,
0
),
problemScores: []
}
}
// 하나의 Problem에 대해 여러 개의 Submission이 존재한다면, 마지막에 제출된 Submission만을 점수 계산에 반영함
const latestSubmissions: {
[problemId: string]: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Field, Int, ObjectType } from '@nestjs/graphql'
import { Language, ResultStatus } from '@admin/@generated'
import { UserContestScoreSummary } from './score-summary'

@ObjectType({ description: 'ContestSubmissionSummaryForUser' })
export class ContestSubmissionSummaryForUser {
@Field(() => UserContestScoreSummary, { nullable: false })
scoreSummary: UserContestScoreSummary

@Field(() => [ContestSubmissionSummaryForOne], { nullable: false })
submissions: ContestSubmissionSummaryForOne[]
}

/**
* 특정 User의 특정 Contest에 대한 Submission 정보 (!== model SubmissionResult)
*/
@ObjectType({ description: 'ContestSubmissionSummaryForOne' })
export class ContestSubmissionSummaryForOne {
@Field(() => Int, { nullable: false })
contestId: number

@Field(() => String, { nullable: false })
problemTitle: string

@Field(() => String, { nullable: false })
username: string

@Field(() => String, { nullable: false })
studentId: string

@Field(() => ResultStatus, { nullable: false })
submissionResult: ResultStatus // Accepted, RuntimeError, ...

@Field(() => Language, { nullable: false })
language: Language

@Field(() => String, { nullable: false })
submissionTime: Date

@Field(() => Int, { nullable: true })
codeSize?: number

@Field(() => String, { nullable: true })
ip?: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
meta {
name: Succeed
type: graphql
seq: 1
}

post {
url: {{gqlUrl}}
body: graphql
auth: none
}

body:graphql {
query getContestSubmissionSummariesByUserId($contestId: Int!, $userId: Int!) {
getContestSubmissionSummaryByUserId(contestId: $contestId, userId: $userId) {
scoreSummary {
contestPerfectScore
problemScores {
problemId
score
}
submittedProblemCount
totalProblemCount
userContestScore
}
submissions {
contestId
problemTitle
studentId
username
submissionResult
language
submissionTime
codeSize
ip
}
}
}

}

body:graphql:vars {
{
"contestId": 1,
"userId": 4
// "problemId": 1
}
}

0 comments on commit 9910124

Please sign in to comment.