diff --git a/config/_examples/_cms/settings.json b/config/_examples/_cms/settings.json new file mode 100644 index 000000000..2973ef405 --- /dev/null +++ b/config/_examples/_cms/settings.json @@ -0,0 +1,6 @@ +{ + "type": "cms", + "activeContest": "ceoi22_5f2", + "otherContests": ["ceoi22_5f1"], + "url": "https://ceoi.hsin.hr/ranking/" +} \ No newline at end of file diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 95d259368..8a5e22f85 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -267,6 +267,60 @@ "url" ] }, + { + "type": "object", + "properties": { + "type": { + "const": "cms" + }, + "url": { + "type": "string" + }, + "activeContest": { + "type": "string" + }, + "otherContests": { + "type": "array", + "items": { + "type": "string" + } + }, + "network": { + "type": "object", + "properties": { + "allowUnsecureConnections": { + "type": "boolean" + } + }, + "additionalProperties": false, + "required": [ + ] + }, + "emulation": { + "type": "object", + "properties": { + "speed": { + "type": "number" + }, + "startTime": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "speed", + "startTime" + ] + } + }, + "additionalProperties": false, + "required": [ + "type", + "url", + "activeContest", + "otherContests" + ] + }, { "type": "object", "properties": { diff --git a/src/cds/src/main/kotlin/org/icpclive/cds/cms/CmsDataSoruce.kt b/src/cds/src/main/kotlin/org/icpclive/cds/cms/CmsDataSoruce.kt new file mode 100644 index 000000000..9babd010f --- /dev/null +++ b/src/cds/src/main/kotlin/org/icpclive/cds/cms/CmsDataSoruce.kt @@ -0,0 +1,116 @@ +package org.icpclive.cds.cms + +import org.icpclive.api.* +import org.icpclive.cds.cms.model.* +import org.icpclive.cds.common.ContestParseResult +import org.icpclive.cds.common.FullReloadContestDataSource +import org.icpclive.cds.common.jsonLoader +import org.icpclive.cds.settings.CmsSettings +import org.icpclive.util.Enumerator +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +internal class CmsDataSource(val settings: CmsSettings) : FullReloadContestDataSource(5.seconds) { + private val contestsLoader = jsonLoader>(settings.network, null) { "${settings.url}/contests/" } + private val tasksLoader = jsonLoader>(settings.network, null) { "${settings.url}/tasks/"} + private val teamsLoader = jsonLoader>(settings.network, null) { "${settings.url}/teams/"} + private val usersLoader = jsonLoader>(settings.network, null) { "${settings.url}/users/"} + private val submissionsLoader = jsonLoader>(settings.network, null) { "${settings.url}/submissions/"} + private val subchangesLoader = jsonLoader>(settings.network, null) { "${settings.url}/subchanges/"} + private val problemId = Enumerator() + private val teamId = Enumerator() + private val submissionId = Enumerator() + + override suspend fun loadOnce(): ContestParseResult { + val contests = contestsLoader.load() + val mainContest = contests[settings.activeContest] ?: error("No data for contest ${settings.activeContest}") + val finishedContestsProblems = mutableSetOf() + val runningContestProblems = mutableSetOf() + val problems = buildList { + val problems = tasksLoader.load().entries.groupBy { it.value.contest }.mapValues { + it.value.map { (k, v) -> + ProblemInfo( + letter = v.short_name, + name = v.name, + id = problemId[k], + contestSystemId = k, + ordinal = 0, + scoreMergeMode = when (v.score_mode) { + ScoreMode.max -> ScoreMergeMode.MAX_TOTAL + ScoreMode.max_subtask -> ScoreMergeMode.MAX_PER_GROUP + }, + minScore = 0.0, + maxScore = v.max_score + ) + } + } + for (other in settings.otherContests) { + for (p in problems[other] ?: emptyList()) { + add(p.copy(ordinal = size)) + finishedContestsProblems.add(p.contestSystemId) + } + } + for (p in problems[settings.activeContest] ?: emptyList()) { + add(p.copy(ordinal = size)) + runningContestProblems.add(p.contestSystemId) + } + } + val organizations = teamsLoader.load().map { (k, v) -> + OrganizationInfo( + cdsId = k, + displayName = v.name, + fullName = v.name + ) + } + val teams = usersLoader.load().map {(k, v) -> + TeamInfo( + id = teamId[k], + fullName = "[${v.team}] ${v.f_name} ${v.l_name}", + displayName = "${v.f_name} ${v.l_name}", + contestSystemId = k, + groups = emptyList(), + hashTag = null, + medias = emptyMap(), + isHidden = false, + isOutOfContest = false, + organizationId = v.team, + customFields = mapOf( + "country" to v.team, + "first_name" to v.f_name, + "last_name" to v.l_name + ) + ) + } + val info = ContestInfo( + name = mainContest.name, + status = ContestStatus.byCurrentTime(mainContest.begin, mainContest.end - mainContest.begin), + resultType = ContestResultType.IOI, + startTime = mainContest.begin, + contestLength = mainContest.end - mainContest.begin, + freezeTime = mainContest.end - mainContest.begin, + problemList = problems, + teamList = teams, + groupList = emptyList(), + organizationList = organizations, + penaltyRoundingMode = PenaltyRoundingMode.ZERO, + ) + val submissions = submissionsLoader.load().mapNotNull { (k, v) -> + if (v.task !in runningContestProblems && v.task !in finishedContestsProblems) { + return@mapNotNull null + } + RunInfo( + id = submissionId[k], + result = null, + percentage = 0.0, + problemId = problemId[v.task], + teamId = teamId[v.user], + time = if (v.task in runningContestProblems) v.time - mainContest.begin else Duration.ZERO + ) + }.associateBy { it.id }.toMutableMap() + subchangesLoader.load().entries.sortedBy { it.value.time }.forEach {(_, it) -> + val r = submissions[submissionId[it.submission]] ?: return@forEach + submissions[r.id] = r.copy(result = IOIRunResult(it.extra.map { it.toDouble() })) + } + return ContestParseResult(info, submissions.values.sortedBy { it.id }, emptyList()) + } +} \ No newline at end of file diff --git a/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Contest.kt b/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Contest.kt new file mode 100644 index 000000000..886b078a3 --- /dev/null +++ b/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Contest.kt @@ -0,0 +1,14 @@ +package org.icpclive.cds.cms.model + +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable +import org.icpclive.util.UnixSecondsSerializer + +@Serializable +data class Contest( + val name: String, + @Serializable(with = UnixSecondsSerializer::class) + val begin: Instant, + @Serializable(with = UnixSecondsSerializer::class) + val end: Instant, +) \ No newline at end of file diff --git a/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Subchange.kt b/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Subchange.kt new file mode 100644 index 000000000..dc65858cb --- /dev/null +++ b/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Subchange.kt @@ -0,0 +1,11 @@ +package org.icpclive.cds.cms.model + +import kotlinx.serialization.Serializable + +@Serializable +class Subchange( + val score: Double, + val submission: String, + val extra: List, + val time: Int +) \ No newline at end of file diff --git a/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Submission.kt b/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Submission.kt new file mode 100644 index 000000000..a437c89a8 --- /dev/null +++ b/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Submission.kt @@ -0,0 +1,13 @@ +package org.icpclive.cds.cms.model + +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable +import org.icpclive.util.UnixSecondsSerializer + +@Serializable +class Submission( + val user: String, + val task: String, + @Serializable(with = UnixSecondsSerializer::class) + val time: Instant +) \ No newline at end of file diff --git a/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Task.kt b/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Task.kt new file mode 100644 index 000000000..56c9835c2 --- /dev/null +++ b/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Task.kt @@ -0,0 +1,20 @@ +package org.icpclive.cds.cms.model + +import kotlinx.serialization.Serializable + +enum class ScoreMode { + max, + max_subtask, +} + +@Serializable +data class Task( + val name: String, + val short_name: String, + val contest: String, + val max_score: Double, + val score_precision: Int, + val extra_headers: List, + val order:Int, + val score_mode: ScoreMode +) \ No newline at end of file diff --git a/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Team.kt b/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Team.kt new file mode 100644 index 000000000..a28043146 --- /dev/null +++ b/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/Team.kt @@ -0,0 +1,6 @@ +package org.icpclive.cds.cms.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Team(val name: String) \ No newline at end of file diff --git a/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/User.kt b/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/User.kt new file mode 100644 index 000000000..06b2bca30 --- /dev/null +++ b/src/cds/src/main/kotlin/org/icpclive/cds/cms/model/User.kt @@ -0,0 +1,10 @@ +package org.icpclive.cds.cms.model + +import kotlinx.serialization.Serializable + +@Serializable +class User( + val f_name: String, + val l_name: String, + val team: String +) \ No newline at end of file diff --git a/src/cds/src/main/kotlin/org/icpclive/cds/settings/CDSSettings.kt b/src/cds/src/main/kotlin/org/icpclive/cds/settings/CDSSettings.kt index e0fa5b982..2590b4ead 100644 --- a/src/cds/src/main/kotlin/org/icpclive/cds/settings/CDSSettings.kt +++ b/src/cds/src/main/kotlin/org/icpclive/cds/settings/CDSSettings.kt @@ -17,6 +17,7 @@ import org.icpclive.cds.atcoder.AtcoderDataSource import org.icpclive.cds.cats.CATSDataSource import org.icpclive.cds.clics.ClicsDataSource import org.icpclive.cds.clics.FeedVersion +import org.icpclive.cds.cms.CmsDataSource import org.icpclive.cds.codedrills.CodeDrillsDataSource import org.icpclive.cds.codeforces.CFDataSource import org.icpclive.cds.common.ContestDataSource @@ -239,6 +240,19 @@ class AtcoderSettings( override fun toDataSource(creds: Map) = AtcoderDataSource(this) } +@SerialName("cms") +@Serializable +class CmsSettings( + val url: String, + val activeContest: String, + val otherContests: List, + override val network: NetworkSettings? = null, + override val emulation: EmulationSettings? = null +) : CDSSettings() { + override fun toDataSource(creds: Map) = CmsDataSource(this) +} + + @OptIn(ExperimentalSerializationApi::class) fun parseFileToCdsSettings(path: Path) : CDSSettings { val file = path.toFile()