From fb7ca386a565e1487d412c2f55147b145cd0b726 Mon Sep 17 00:00:00 2001 From: Pavel Kunyavskiy Date: Sat, 9 Dec 2023 00:01:53 +0100 Subject: [PATCH] Eolymp practice session fixes --- src/cds/src/main/graphql/eolymp.graphql | 191 ++++++++++++++---- .../queries/JudgeContestSubmissions.graphql | 2 + .../icpclive/cds/eolymp/EOlympDataSource.kt | 55 ++++- 3 files changed, 202 insertions(+), 46 deletions(-) diff --git a/src/cds/src/main/graphql/eolymp.graphql b/src/cds/src/main/graphql/eolymp.graphql index a70be1e26..0287155e9 100644 --- a/src/cds/src/main/graphql/eolymp.graphql +++ b/src/cds/src/main/graphql/eolymp.graphql @@ -33,7 +33,7 @@ union Content = ECM | HTML | Latex | Markdown union CourseEntryContent = CourseEntryDocument | CourseEntrySection -union IdentityProvider = IdentityProviderEolymp | IdentityProviderLocal | IdentityProviderOIDC +union IdentityConfigProvider = IdentityConfigProviderBasecamp | IdentityConfigProviderLocal | IdentityConfigProviderOIDC union RankerScoreboardColumnTarget = CommunityAttribute | JudgeContest | JudgeProblem @@ -154,12 +154,14 @@ type AtlasProblem { private: Boolean! "Score for a given user" score(memberId: ID, userId: ID): AtlasScore - "Problem's solutions" + solution(id: ID!): AtlasSolution solutions(after: String, filters: AtlasSolutionFilter, first: Int, order: Direction, sort: AtlasSolutionSort): AtlasSolutionConnection "Problem statement by locale preference" statement(locale: String, preferredLocales: [String!], render: Boolean): AtlasStatement "All problem's statements" statements(render: Boolean): [AtlasStatement!] + suggestion(id: ID!): AtlasSuggestion + suggestions(after: String, filters: AtlasSuggestionFilter, first: Int, order: Direction, sort: AtlasSuggestionSort): AtlasSuggestionConnection "Problem's code templates" templates: AtlasTemplateConnection "Problem's testset by ID" @@ -253,6 +255,7 @@ type AtlasSubmission { status: String! submittedAt: Time! user: CognitoUser + verdict: String! wallTimeUsage: Int! } @@ -279,6 +282,7 @@ type AtlasSubmissionGroup { score: Float! scoringMode: String! status: String! + verdict: String! wallTimeUsage: Int! } @@ -291,9 +295,36 @@ type AtlasSubmissionRun { resourceUsage: Float! score: Float! status: String! + verdict: String! wallTimeUsage: Int! } +type AtlasSuggestion { + createdAt: Time! + difficulty: Int! + editorial: Content! + id: ID! + locale: String + member: CommunityMember + statement: Content! + status: String! + title: String + topics(locale: String): [TaxonomyValue!]! + updatedAt: Time! +} + +type AtlasSuggestionConnection { + edges: [AtlasSuggestionEdge!]! + nodes: [AtlasSuggestion!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type AtlasSuggestionEdge { + cursor: String! + node: AtlasSuggestion! +} + type AtlasTemplate { footer: String header: String @@ -417,6 +448,13 @@ type CognitoUser { usernameChangedOn: Time } +type CommercePrice { + currency: String! + id: ID! + recurrence: String! + unitAmount: Int! +} + type CommunityAttribute { choices: [String!]! description(locale: String, preferredLocales: [String!]): CommunityAttributeDescription! @@ -462,12 +500,32 @@ type CommunityGhost { name: String! } +type CommunityGroup { + description: String! + id: ID! + name: String! +} + +type CommunityGroupConnection { + edges: [CommunityGroupEdge!]! + nodes: [CommunityGroup!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type CommunityGroupEdge { + cursor: String! + node: CommunityGroup! +} + type CommunityMember { account: CommunityMemberAccount! active: Boolean! attributes: [CommunityAttributeValue!]! disabled: Boolean! @deprecated(reason : "use active instead") + fallbackTier: CommunityTier ghost: Boolean! @deprecated(reason : "use account instead") + groups: [CommunityGroup!]! id: ID! incomplete: Boolean! name: String! @@ -476,6 +534,7 @@ type CommunityMember { registered: Boolean! @deprecated(reason : "use incomplete instead") staffed: Boolean! @deprecated(reason : "use account instead") status: String! @deprecated(reason : "use active, unofficial and account flags instead") + tier: CommunityTier unofficial: Boolean! users: [CommunityMember!]! @deprecated(reason : "use account instead") } @@ -498,6 +557,27 @@ type CommunityTeam { staffed: Boolean! } +type CommunityTier { + description: Content + id: ID! + image: String! + name: String! + prices(currency: String): [CommercePrice!] + summary: String! +} + +type CommunityTierConnection { + edges: [CommunityTierEdge!]! + nodes: [CommunityTier!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type CommunityTierEdge { + cursor: String! + node: CommunityTier! +} + type CommunityUser { birthday: Time! city: String! @@ -522,14 +602,6 @@ type CommunityUserPreferences { timezone: String! } -type Config { - attribute(key: String!): CommunityAttribute - attributes: CommunityAttributeConnection - auth: AuthConfig - identityProvider: IdentityProvider - visibility: String! -} - type ContentFragment { content: Content id: ID! @@ -698,9 +770,10 @@ type ExecutorRuntime { } type Geography { - countries: GeographyCountryConnection + countries(after: String, filters: GeographyCountryFilter, first: Int): GeographyCountryConnection country(id: ID!): GeographyCountry region(id: ID!): GeographyRegion + regions(after: String, filters: GeographyRegionFilter, first: Int): GeographyRegionConnection } type GeographyCountry { @@ -743,18 +816,30 @@ type HTML { html: String! } -type IdentityProviderEolymp { - issuer: String! +type IdentityConfig { + displayNameAttribute: String! + displayNameType: String! + provider: IdentityConfigProvider } -type IdentityProviderLocal { - issuer: String! +type IdentityConfigProviderBasecamp { + basecamp: Boolean! } -type IdentityProviderOIDC { +type IdentityConfigProviderLocal { + allowModifyBasics: Boolean! + allowModifyEmail: Boolean! + allowModifyNickname: Boolean! + allowModifyPassword: Boolean! +} + +type IdentityConfigProviderOIDC { authorizeEndpoint: String! + clientId: String! + clientSecret: String! issuer: String! keysEndpoint: String! + redirectUri: String! tokenEndpoint: String! userinfoEndpoint: String! } @@ -1119,14 +1204,13 @@ type JudgeSubmission { cost: Float! deleted: Boolean! error: String - "deprecated use groups" groups: [JudgeSubmissionGroup!]! id: ID! lang: String! participant: JudgeParticipant percentage: Float! problem: JudgeProblem - runs: [JudgeSubmissionRun!]! + runs: [JudgeSubmissionRun!]! @deprecated(reason : "deprecated use groups") runtime: ExecutorRuntime score: Float! signature: String! @@ -1134,6 +1218,7 @@ type JudgeSubmission { source: String! status: String! submittedAt: Time! + verdict: String! } type JudgeSubmissionConnection { @@ -1156,23 +1241,22 @@ type JudgeSubmissionGroup { runs: [JudgeSubmissionRun!]! score: Float! status: String! + verdict: String! wallTimeUsage: Int! } type JudgeSubmissionRun { - "deprecated, use cost" cost: Float! cpuTimeUsage: Int! id: ID! - "deprecated, use index" index: Int! memoryUsage: Int! score: Float! status: String! - testId: ID! - "deprecated, don't use" - testIndex: Int! - testScore: Float! + testId: ID! @deprecated(reason : "don't use") + testIndex: Int! @deprecated(reason : "use index") + testScore: Float! @deprecated(reason : "use cost") + verdict: String! wallTimeUsage: Int! } @@ -1224,12 +1308,11 @@ type Markdown { } type Meta { + homeUrl: String! id: ID! image: String! + issuerUrl: String! key: String! - maxTeamSize: Int! - membership: String! - minTeamSize: Int! name: String! plan: String! quota: UniverseSpaceQuota @@ -1252,7 +1335,10 @@ type PageInfo { } type Query { - config: Config! + acl: ACL! + attribute(key: String!): CommunityAttribute + attributes: CommunityAttributeConnection + authConfig: AuthConfig contest(id: ID!): JudgeContest! contests(after: String, filters: JudgeContestFilter, first: Int): JudgeContestConnection course(id: ID!, render: Boolean): Course @@ -1263,6 +1349,9 @@ type Query { fragment(id: ID!, render: Boolean): ContentFragment fragments(after: String, filters: ContentFragmentFilter, first: Int, render: Boolean): ContentFragmentConnection geography: Geography! + group(id: ID!): CommunityGroup! + groups(after: String, filters: CommunityGroupFilter, first: Int): CommunityGroupConnection + identityConfig: IdentityConfig keeper: Keeper! member(id: ID!): CommunityMember! members(after: String, filters: CommunityMemberFilter, first: Int): CommunityMemberConnection @@ -1274,13 +1363,13 @@ type Query { problems(after: String, filters: AtlasProblemFilter, first: Int, order: Direction, sort: AtlasProblemSort): AtlasProblemConnection scoreboard(id: ID, key: String): RankerScoreboard scoreboards(after: String, filters: RankerScoreboardFilter, first: Int): RankerScoreboardConnection - self: Self! @deprecated(reason : "use viewer instead") - space: UniverseSpace! @deprecated(reason : "use meta and other root nodes instead") submission(id: ID!): AtlasSubmission submissions(after: String, filters: AtlasSubmissionFilter, first: Int): AtlasSubmissionConnection taxonomy: Taxonomy! ticket(id: ID!): JudgeTicket! tickets(after: String, filters: JudgeTicketFilter, first: Int, order: Direction, sort: JudgeTicketSort): JudgeTicketConnection + tier(id: ID!): CommunityTier! + tiers(after: String, first: Int): CommunityTierConnection viewer: Claims } @@ -1444,10 +1533,6 @@ type RankerScoreboardRowValueString { string: String! } -type Self { - user: CognitoUser -} - type Subscription { watchContestSubmission(contestId: ID!, submissionId: ID!): JudgeSubmission watchSubmission(id: ID!): AtlasSubmission @@ -1495,15 +1580,14 @@ type UniverseSpace { entitlements: [String!] fragment(id: ID!, render: Boolean): ContentFragment fragments(after: String, filters: ContentFragmentFilter, first: Int, render: Boolean): ContentFragmentConnection + homeUrl: String! id: ID! image: String! introspect: CommunityMember + issuerUrl: String! key: String! - maxTeamSize: Int! member(id: ID!): CommunityMember! members(after: String, filters: CommunityMemberFilter, first: Int): CommunityMemberConnection - membership: String! - minTeamSize: Int! name: String! parents(locale: String, locales: [String!], path: String!, render: Boolean): [ContentFragment!]! path(locale: String, locales: [String!], path: String!, render: Boolean): ContentFragment @@ -1604,6 +1688,11 @@ enum AtlasSolutionSort { type } +enum AtlasSuggestionSort { + created_at + updated_at +} + enum CourseSort { default } @@ -1674,6 +1763,12 @@ input AtlasSubmissionFilter { userId: IDExpression } +input AtlasSuggestionFilter { + id: IDExpression + memberId: IDExpression + status: EnumExpression +} + input AtlasVersionFilter { changeOp: EnumExpression changePath: StringExpression @@ -1688,8 +1783,15 @@ input BooleanExpression { eq: Boolean } +input CommunityGroupFilter { + id: IDExpression + name: StringExpression + query: String +} + input CommunityMemberFilter { active: BooleanExpression + groupId: IDExpression id: IDExpression incomplete: BooleanExpression name: StringExpression @@ -1788,6 +1890,19 @@ input FloatExpression { neq: Float } +input GeographyCountryFilter { + id: IDExpression + name: StringExpression + query: String +} + +input GeographyRegionFilter { + countryId: IDExpression + id: IDExpression + name: StringExpression + query: String +} + "Filter expression for ID type" input IDExpression { "Equals" @@ -1826,6 +1941,7 @@ input JudgeContestFilter { country: EnumExpression difficulty: IntExpression endsAt: TimeExpression + featured: BooleanExpression format: EnumExpression id: IDExpression name: StringExpression @@ -1836,6 +1952,7 @@ input JudgeContestFilter { scale: EnumExpression series: EnumExpression startsAt: TimeExpression + status: EnumExpression visibility: EnumExpression year: IntExpression } diff --git a/src/cds/src/main/graphql/queries/JudgeContestSubmissions.graphql b/src/cds/src/main/graphql/queries/JudgeContestSubmissions.graphql index 68fda8b41..baa971c40 100644 --- a/src/cds/src/main/graphql/queries/JudgeContestSubmissions.graphql +++ b/src/cds/src/main/graphql/queries/JudgeContestSubmissions.graphql @@ -12,6 +12,8 @@ query JudgeContestSubmissions($id: ID!, $after: String, $count: Int!) { problem { id } participant { id } submittedAt + verdict + status } } } diff --git a/src/cds/src/main/kotlin/org/icpclive/cds/eolymp/EOlympDataSource.kt b/src/cds/src/main/kotlin/org/icpclive/cds/eolymp/EOlympDataSource.kt index 31b78968a..26f013483 100644 --- a/src/cds/src/main/kotlin/org/icpclive/cds/eolymp/EOlympDataSource.kt +++ b/src/cds/src/main/kotlin/org/icpclive/cds/eolymp/EOlympDataSource.kt @@ -3,11 +3,13 @@ package org.icpclive.cds.eolymp import com.eolymp.graphql.* import com.expediagroup.graphql.client.ktor.GraphQLKtorClient import com.expediagroup.graphql.client.types.GraphQLClientRequest +import jdk.jfr.Percentage import kotlinx.datetime.toKotlinInstant import org.icpclive.api.* import org.icpclive.cds.common.* import org.icpclive.cds.settings.EOlympSettings import org.icpclive.util.Enumerator +import org.icpclive.util.getLogger import java.net.URL import java.time.chrono.IsoChronology import java.time.format.DateTimeFormatterBuilder @@ -56,18 +58,24 @@ private suspend fun GraphQLKtorClient.teams(contestId: String, after: String?, c internal class EOlympDataSource(val settings: EOlympSettings) : FullReloadContestDataSource(5.seconds) { - val graphQLClient = GraphQLKtorClient( + private val graphQLClient = GraphQLKtorClient( URL(settings.url), defaultHttpClient(ClientAuth.Bearer(settings.token.value), settings.network) ) - fun converStatus(status: String) = ContestStatus.OVER.also { status.let { } } - fun convertResultType(format: String) = when (format) { + private fun convertStatus(status: String) = when (status) { + "STATUS_UNKNOWN" -> error("Doc said $status should not be used") + "SCHEDULED" -> ContestStatus.BEFORE + "OPEN", "SUSPENDED", "FROZEN" -> ContestStatus.RUNNING + "COMPLETE" -> ContestStatus.OVER + else -> error("Unknown status: $status") + } + private fun convertResultType(format: String) = when (format) { "ICPC" -> ContestResultType.ICPC else -> error("Unknown contest format: $format") } - val dateTimeFormatter = DateTimeFormatterBuilder() + private val dateTimeFormatter = DateTimeFormatterBuilder() .parseCaseInsensitive() .appendValue(ChronoField.YEAR, 4) .appendLiteral('-') @@ -122,7 +130,7 @@ internal class EOlympDataSource(val settings: EOlympSettings) : FullReloadContes } val contestInfo = ContestInfo( name = result.name, - status = converStatus(result.status), + status = convertStatus(result.status), resultType = convertResultType(result.format), startTime = parseTime(result.startsAt), contestLength = result.duration.seconds, @@ -154,10 +162,7 @@ internal class EOlympDataSource(val settings: EOlympSettings) : FullReloadContes addAll(x.submissions!!.nodes.map { RunInfo( id = runIds[it.id], - result = when { - it.percentage == 1.0 -> Verdict.Accepted - else -> Verdict.Rejected - }.toRunResult(), + result = parseVerdict(it.status, it.verdict, it.percentage)?.toRunResult(), percentage = 0.0, problemId = problemIds[it.problem!!.id], teamId = teamIds[it.participant!!.id], @@ -176,6 +181,38 @@ internal class EOlympDataSource(val settings: EOlympSettings) : FullReloadContes ) } + private fun parseVerdict(status: String, verdict: String, percentage: Double) : Verdict? { + return when (status) { + "ERROR" -> Verdict.CompilationError + "PENDING", "TESTING" -> null + "COMPLETE" -> { + when (verdict) { + "NO_VERDICT" -> when { + percentage == 1.0 -> Verdict.Accepted + else -> Verdict.Rejected + } + "ACCEPTED" -> Verdict.Accepted + "WRONG_ANSWER" -> Verdict.WrongAnswer + "TIME_LIMIT_EXCEEDED" -> Verdict.IdlenessLimitExceeded + "CPU_EXHAUSTED" -> Verdict.TimeLimitExceeded + "MEMORY_OVERFLOW" -> Verdict.MemoryLimitExceeded + "RUNTIME_ERROR" -> Verdict.RuntimeError + else -> { + log.info("Unknown verdict: $verdict, assuming rejected") + Verdict.Rejected + } + } + } + else -> { + log.info("Unexpected submission status: $status, assuming untested") + null + } + } + } + private fun parseTime(s: String) = java.time.Instant.from(dateTimeFormatter.parse(s)).toKotlinInstant() + companion object { + val log = getLogger(EOlympDataSource::class) + } } \ No newline at end of file