Skip to content

Commit

Permalink
feat: support contest status (#127)
Browse files Browse the repository at this point in the history
* feat: support contest status

* chore: regenerate lockfile

* chore: remove useless optional
  • Loading branch information
thezzisu authored Feb 8, 2025
1 parent 2131c18 commit 50f686d
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 22 deletions.
3 changes: 3 additions & 0 deletions .yarn/versions/039c134b.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
releases:
"@aoi-js/frontend": patch
"@aoi-js/server": patch
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
</ListInput>
</template>
</OptionalInput>
<VCheckbox v-model="model.disableCreateSolution" :label="t('disable-create-solution')" />
<VCheckbox v-model="model.disableSubmit" :label="t('disable-submit')" />
</template>

Expand All @@ -36,7 +37,9 @@ const model = defineModel<IContestProblemSettings>({ required: true })
en:
solution-count-limit: Solution Count Limit
disable-submit: Disable Submit
disable-create-solution: Disable Create Solution
zh-Hans:
solution-count-limit: 提交记录数量限制
disable-submit: 禁止提交
disable-create-solution: 禁止创建解答
</i18n>
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const keys: {
registrationAllowPublic: 'registration-allow-public',
problemEnabled: 'problem-enabled',
problemShowTags: 'problem-show-tags',
problemAllowCreateSolution: 'problem-allow-create-solution',
solutionEnabled: 'solution-enabled',
solutionAllowSubmit: 'solution-allow-submit',
solutionShowOther: 'solution-show-other',
Expand All @@ -61,7 +62,8 @@ const keys: {
solutionShowOtherData: 'solution-show-other-data',
ranklistEnabled: 'ranklist-enabled',
ranklistSkipCalculation: 'ranklist-skip-calculation',
participantEnabled: 'participant-enabled'
participantEnabled: 'participant-enabled',
forceRunning: 'force-running'
}
const entries = Object.entries(keys).map(([k, v]) => [
Expand All @@ -78,6 +80,7 @@ en:
registration-allow-public: Registration Allow Public
problem-enabled: Problem Enabled
problem-show-tags: Problem Show Tags
problem-allow-create-solution: Problem Allow Create Solution
solution-enabled: Solution Enabled
solution-allow-submit: Solution Allow Submit
solution-show-other: Solution Allow Other
Expand All @@ -87,11 +90,13 @@ en:
ranklist-enabled: Ranklist Enabled
ranklist-skip-calculation: Skip Ranklist Calculation
participant-enabled: Participant Enabled
force-running: Force Running
contest-stage-settings-hint:
registration-enabled: Enable registration
registration-allow-public: Allow public registration
problem-enabled: Enable problem
problem-show-tags: Show tags on problem
problem-allow-create-solution: Allow create solution
solution-enabled: Enable solution
solution-allow-submit: Allow submit
solution-show-other: Allow show other
Expand All @@ -101,12 +106,14 @@ en:
ranklist-enabled: Enable ranklist
ranklist-skip-calculation: Skip ranklist calculation
participant-enabled: Enable participant
force-running: Force running
zh-Hans:
contest-stage-settings:
registration-enabled: 启用注册功能
registration-allow-public: 允许公开注册
problem-enabled: 启用题目
problem-show-tags: 题目显示标签
problem-allow-create-solution: 允许创建解答
solution-enabled: 启用提交记录
solution-allow-submit: 允许提交
solution-show-other: 允许展示他人提交记录
Expand All @@ -116,11 +123,13 @@ zh-Hans:
ranklist-enabled: 启用排名
ranklist-skip-calculation: 跳过排名计算
participant-enabled: 启用参赛者
force-running: 强制为进行状态
contest-stage-settings-hint:
registration-enabled: 开启后,选手才可以报名
registration-allow-public: 关闭后,只允许分配了报名权限的用户报名
problem-enabled: 开启后,选手才可查看题目
problem-show-tags: 关闭后,题目隐藏标签
problem-allow-create-solution: 开启后,选手才可以创建解答
solution-enabled: 开启后,选手才可提交或查看提交
solution-allow-submit: 开启后,选手才可以提交
solution-show-other: 开启后,选手可以查看他人提交记录(仅状态)
Expand All @@ -130,4 +139,5 @@ zh-Hans:
ranklist-enabled: 开启后,选手才可以查看排行榜
ranklist-skip-calculation: 开启后,本阶段提交不计入排行榜(订正模式)
participant-enabled: 开启后,选手才可以查看参赛者列表
force-running: 若不开启,当启用题目、允许提交、计算排行时方为进行状态
</i18n>
3 changes: 2 additions & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"lib"
],
"bin": {
"aoi-server": "lib/cli/index.js"
"aoi-server": "lib/cli/index.js",
"aoi-server-updater": "lib/cli/updater.js"
},
"dependencies": {
"@aoi-js/common": "workspace:^",
Expand Down
139 changes: 139 additions & 0 deletions apps/server/src/cli/updater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { fastify } from 'fastify'

import { authProviderPlugin } from '../auth/index.js'
import { cachePlugin } from '../cache/index.js'
import { ContestStatus, dbPlugin } from '../db/index.js'
import { logger } from '../utils/logger.js'

const server = fastify({ loggerInstance: logger })

await server.register(dbPlugin)
await server.register(cachePlugin)
await server.register(authProviderPlugin)

server.post('/contest/update-status', async () => {
const { db } = server
const now = Date.now()
await db.contests.updateMany(
// Find contests that need status update
{ $or: [{ nextStatusUpdate: { $lte: now } }, { nextStatusUpdate: { $exists: false } }] },
[
{
$set: {
nextStatusUpdate: {
$let: {
vars: {
// Find the next stage that will start
futureStages: {
$filter: {
input: '$stages',
cond: {
$lt: [{ $convert: { input: '$$NOW', to: 'double' } }, '$$this.start']
},
limit: 1
}
}
},
in: {
$cond: {
// If there is a future stage, update status when it starts
if: { $gte: [{ $size: '$$futureStages' }, 1] },
// Use the start time of the next stage
then: {
$getField: { field: 'start', input: { $arrayElemAt: ['$$futureStages', 0] } }
},
// Otherwise, never update status again
else: Infinity
}
}
}
},
status: {
$let: {
vars: {
effectiveStages: {
$filter: {
input: '$stages',
cond: {
$lte: [{ $convert: { input: '$$NOW', to: 'double' } }, '$$this.end']
},
limit: 1
}
}
},
in: {
$let: {
vars: {
stage: { $arrayElemAt: ['$$effectiveStages', -1] }
},
in: {
$let: {
vars: {
isLastStage: {
$eq: [{ $size: '$$effectiveStages' }, { $size: '$stages' }]
},
isRunning: {
$or: [
{ $eq: ['$$stage.settings.forceRunning', true] },
{
$and: [
{ $eq: ['$$stage.settings.problemEnabled', true] },
{ $eq: ['$$stage.settings.problemAllowCreateSolution', true] },
{ $ne: ['$$stage.settings.ranklistSkipCalculation', true] }
]
}
]
}
},
in: {
$cond: {
if: '$$isRunning',
then: ContestStatus.RUNNING,
else: {
$cond: {
if: '$$isLastStage',
then: ContestStatus.ENDED,
else: ContestStatus.PENDING
}
}
}
}
}
}
}
}
}
}
}
}
]
)
return 0
})

const startAsyncInterval = (fn: CallableFunction, interval: number) => {
let cancelled = false
const run = async () => {
while (!cancelled) {
await fn()
await new Promise((resolve) => setTimeout(resolve, interval))
}
}
run()
return () => {
cancelled = true
}
}

// TODO: allow custom interval
startAsyncInterval(async () => {
const resp = await server.inject({
method: 'POST',
url: '/contest/update-status'
})
if (resp.statusCode !== 200) {
server.log.error(`Failed to update contest status: ${resp.payload}`)
return
}
server.log.info('Contest status updated')
}, 1000)
9 changes: 9 additions & 0 deletions apps/server/src/db/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ export const contestRuleSchemas = {
solution: SContestSolutionRuleResult
}

export enum ContestStatus {
PENDING = 0,
ENDED = 1,
RUNNING = 2
}

export interface IContest
extends IPrincipalControlable,
IWithAttachment,
Expand All @@ -102,6 +108,9 @@ export interface IContest

participantCount: number

status?: ContestStatus
nextStatusUpdate?: number

rules?: RulesFromSchemas<
typeof contestRuleSchemas,
{
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/routes/contest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export const contestRoutes = defineRoutes(async (s) => {
participantCount: 1
}
},
{ start: -1 }
{ status: -1, start: -1 }
)
return result
}
Expand Down Expand Up @@ -225,7 +225,7 @@ export const contestRoutes = defineRoutes(async (s) => {
participantCount: 1
}
},
{ start: -1 }
{ status: -1, start: -1 }
)
return result
}
Expand Down
33 changes: 22 additions & 11 deletions apps/server/src/routes/contest/problem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,26 +164,37 @@ const problemViewRoutes = defineRoutes(async (s) => {
async (req, rep) => {
const ctx = req.inject(kContestContext)
if (!ctx._contestParticipant) return rep.forbidden()
const { solutionEnabled } = ctx._contestStage.settings
if (!solutionEnabled && !hasCapability(ctx._contestCapability, CONTEST_CAPS.CAP_ADMIN)) {
const { solutionEnabled, problemAllowCreateSolution } = ctx._contestStage.settings
// Check for contest settings
if (
!(solutionEnabled && problemAllowCreateSolution) &&
!hasCapability(ctx._contestCapability, CONTEST_CAPS.CAP_ADMIN)
) {
return rep.forbidden()
}

const org = await orgs.findOne(
{ _id: ctx._contest.orgId },
{ projection: { 'settings.oss': 1 } }
)
const oss = org?.settings.oss
if (!oss) return rep.preconditionFailed('OSS not configured')

// Check for problem settings
const [problemId, settings] = loadProblemSettings(req)
if (!settings) return rep.notFound()
if (!settings) {
return rep.notFound()
}
if (
!hasCapability(ctx._contestCapability, CONTEST_CAPS.CAP_ADMIN) &&
settings.showAfter &&
settings.showAfter > req._now
)
) {
return rep.notFound()
}
if (settings.disableCreateSolution) {
return rep.forbidden()
}

const org = await orgs.findOne(
{ _id: ctx._contest.orgId },
{ projection: { 'settings.oss': 1 } }
)
const oss = org?.settings.oss
if (!oss) return rep.preconditionFailed('OSS not configured')

const problem = await problems.findOne(
{ _id: problemId },
Expand Down
17 changes: 10 additions & 7 deletions apps/server/src/schemas/contest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const SContestStage = T.StrictObject({
problemEnabled: T.Boolean(),
// Show problem tags to participants
problemShowTags: T.Boolean(),
// Allow participant to create solutions
problemAllowCreateSolution: T.Boolean(),
// Show solutions to participants
solutionEnabled: T.Boolean(),
// Allow submit new solutions
Expand All @@ -41,15 +43,15 @@ export const SContestStage = T.StrictObject({
// Show participants panel
participantEnabled: T.Boolean(),
// Participant tag rules
tagRules: T.Optional(
T.Partial(
T.StrictObject({
copyVerifiedFields: T.String()
})
)
tagRules: T.Partial(
T.StrictObject({
copyVerifiedFields: T.String()
})
),
// Actions
actions: T.Optional(T.Array(SContestAction))
actions: T.Array(SContestAction),
// Force Running
forceRunning: T.Boolean()
})
)
})
Expand All @@ -62,6 +64,7 @@ export const SContestProblemSettings = T.StrictObject({
solutionCountLimit: T.Integer(),
showAfter: T.Optional(T.Integer()),
actions: T.Optional(T.Array(SContestAction)),
disableCreateSolution: T.Optional(T.Boolean()),
disableSubmit: T.Optional(T.Boolean())
})

Expand Down
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ __metadata:
optional: true
bin:
aoi-server: lib/cli/index.js
aoi-server-updater: lib/cli/updater.js
languageName: unknown
linkType: soft

Expand Down

0 comments on commit 50f686d

Please sign in to comment.