Skip to content

Commit 0568853

Browse files
committed
Support org custom fields
1 parent 8e802ae commit 0568853

File tree

9 files changed

+113
-103
lines changed

9 files changed

+113
-103
lines changed

src/backend/src/main/kotlin/org/icpclive/admin/Routing.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,15 @@ fun Route.configureAdminApiRouting() {
205205
examplesPackage = null
206206
)
207207
}
208+
route("/orgCustomFields") {
209+
configureConfigFileRouting(
210+
Config.cdsSettings.orgCustomFieldsCsvPath,
211+
emptyResponse = "",
212+
validate = { },
213+
schemaLocation = null,
214+
examplesPackage = null
215+
)
216+
}
208217

209218

210219
get("/contestInfo") {

src/cds/cli/api/cli.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ public class org/icpclive/cds/cli/CdsCommandLineOptions : com/github/ajalt/clikt
44
public final fun getConfigDirectory ()Ljava/nio/file/Path;
55
public final fun getCredentialFile ()Ljava/nio/file/Path;
66
public final fun getCustomFieldsCsvPath ()Ljava/nio/file/Path;
7+
public final fun getOrgCustomFieldsCsvPath ()Ljava/nio/file/Path;
78
public final fun toFlow ()Lkotlinx/coroutines/flow/Flow;
89
}
910

src/cds/cli/src/main/kotlin/org/icpclive/cds/cli/CdsCommandLineOptions.kt

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,18 @@ import com.github.ajalt.clikt.parameters.groups.OptionGroup
44
import com.github.ajalt.clikt.parameters.options.*
55
import com.github.ajalt.clikt.parameters.types.path
66
import kotlinx.coroutines.Dispatchers
7-
import kotlinx.coroutines.flow.Flow
8-
import kotlinx.coroutines.flow.flowOn
7+
import kotlinx.coroutines.flow.*
98
import kotlinx.serialization.SerializationException
109
import kotlinx.serialization.json.*
1110
import org.apache.commons.csv.CSVFormat
1211
import org.apache.commons.csv.CSVParser
1312
import org.icpclive.cds.ContestUpdate
14-
import org.icpclive.cds.adapters.applyCustomFieldsMap
1513
import org.icpclive.cds.adapters.applyTuningRules
16-
import org.icpclive.cds.api.toTeamId
1714
import org.icpclive.cds.settings.*
1815
import org.icpclive.cds.tunning.TuningRule
1916
import org.icpclive.cds.util.fileContentFlow
2017
import org.icpclive.cds.util.getLogger
18+
import java.io.InputStream
2119
import java.nio.file.Path
2220
import java.nio.file.Paths
2321
import kotlin.io.path.exists
@@ -38,10 +36,14 @@ public open class CdsCommandLineOptions : OptionGroup("CDS options") {
3836
.path(mustExist = true, canBeFile = true, canBeDir = false)
3937
.defaultLazy("configDirectory/advanced.json") { configDirectory.resolve("advanced.json") }
4038

41-
public val customFieldsCsvPath: Path by option("--custom-fields-csv", help = "Path to file with custom fields")
39+
public val customFieldsCsvPath: Path by option("--custom-fields-csv", help = "Path to file with custom fields for teams")
4240
.path(mustExist = true, canBeFile = true, canBeDir = false)
4341
.defaultLazy("configDirectory/custom-fields.csv") { configDirectory.resolve("custom-fields.csv") }
4442

43+
public val orgCustomFieldsCsvPath: Path by option("--org-custom-fields-csv", help = "Path to file with custom fields for organizations")
44+
.path(mustExist = true, canBeFile = true, canBeDir = false)
45+
.defaultLazy("configDirectory/org-custom-fields.csv") { configDirectory.resolve("org-custom-fields.csv") }
46+
4547
public fun toFlow(): Flow<ContestUpdate> {
4648
val advancedProperties = fileContentFlow(
4749
advancedJsonPath,
@@ -61,39 +63,58 @@ public open class CdsCommandLineOptions : OptionGroup("CDS options") {
6163
}
6264
val customFields = fileContentFlow(
6365
customFieldsCsvPath,
64-
noData = emptyMap()
66+
noData = emptyList()
6567
) {
66-
val parser = CSVParser.parse(it.reader(), CSVFormat.EXCEL.builder().setHeader().setSkipHeaderRecord(true).get())
67-
val names = parser.headerNames
68-
if (names.isEmpty()) {
69-
log.warning { "Ignoring malformed ${customFieldsCsvPath.name}: empty file" }
70-
emptyMap()
71-
} else if (names[0] != "team_id") {
72-
log.warning { "Ignoring malformed ${customFieldsCsvPath.name}: first column should be team_id" }
73-
emptyMap()
74-
} else {
75-
parser.records.associate { record ->
76-
record[0].toTeamId() to names.zip(record).drop(1).associate { it.first!! to it.second!! }
77-
}
78-
}
68+
val parsed = parseCsv(customFieldsCsvPath, it, "team_id")
69+
listOf(TuningRule.fromTeamFields(parsed))
70+
}
71+
val orgCustomFields = fileContentFlow(
72+
orgCustomFieldsCsvPath,
73+
noData = emptyList()
74+
) {
75+
val parsed = parseCsv(orgCustomFieldsCsvPath, it, "org_id")
76+
listOf(TuningRule.fromOrganizationFields(parsed))
77+
}
78+
79+
val combinedTuningFlow = combine(customFields, orgCustomFields, advancedProperties) { a, b, c ->
80+
a + b + c
7981
}
8082
log.info { "Using config directory ${this.configDirectory}" }
8183
log.info { "Current working directory is ${Paths.get("").toAbsolutePath()}" }
82-
val path = this.configDirectory.resolve("events.properties")
83-
.takeIf { it.exists() }
84-
?.also { log.warning { "Using events.properties is deprecated, use settings.json instead." } }
84+
val path = this.configDirectory.resolve("events.properties").takeIf { it.exists() }
8585
?: this.configDirectory.resolve("settings.json5").takeIf { it.exists() }
8686
?: this.configDirectory.resolve("settings.json")
8787
val creds: Map<String, String> = this.credentialFile?.let { Json.decodeFromStream<Map<String, String>?>(it.toFile().inputStream()) } ?: emptyMap()
8888
return CDSSettings.fromFile(path) { creds[it] }
8989
.toFlow()
90-
.applyCustomFieldsMap(customFields)
91-
.applyTuningRules(advancedProperties)
90+
.applyTuningRules(combinedTuningFlow)
9291
.flowOn(Dispatchers.IO)
9392
}
93+
9494
private companion object {
9595
val log by getLogger()
9696
}
97+
98+
private fun parseCsv(
99+
path: Path,
100+
input: InputStream,
101+
idHeaderName: String,
102+
): Map<String, Map<String, String>> {
103+
val parser = CSVParser.parse(input.reader(), CSVFormat.EXCEL.builder().setHeader().setSkipHeaderRecord(true).get())
104+
val names = parser.headerNames
105+
if (names.isEmpty()) {
106+
log.warning { "Ignoring malformed ${path.name}: empty file" }
107+
return emptyMap()
108+
}
109+
if (names[0] != idHeaderName) {
110+
log.warning { "Ignoring malformed ${path.name}: first column should be ${idHeaderName}" }
111+
return emptyMap()
112+
}
113+
return parser.records.associate { record ->
114+
record[0] to names.zip(record).drop(1).associate { it.first!! to it.second!! }
115+
}
116+
}
117+
97118
}
98119

99120

src/cds/core/api/core.api

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ public final class org/icpclive/cds/adapters/AdaptersKt {
6565
public static final fun addFirstToSolves (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;
6666
public static final fun addPreviousDaysByResults (Lkotlinx/coroutines/flow/Flow;Ljava/util/List;)Lkotlinx/coroutines/flow/Flow;
6767
public static final fun addPreviousDaysBySettings (Lkotlinx/coroutines/flow/Flow;Ljava/util/List;)Lkotlinx/coroutines/flow/Flow;
68-
public static final fun applyCustomFieldsMap (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;
6968
public static final fun applyTuningRules (Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;
7069
public static final fun autoCreateMissingGroupsAndOrgs (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;
7170
public static final fun calculateScoreDifferences (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;
@@ -3006,6 +3005,8 @@ public abstract interface class org/icpclive/cds/tunning/TuningRule {
30063005
}
30073006

30083007
public final class org/icpclive/cds/tunning/TuningRule$Companion {
3008+
public final fun fromOrganizationFields (Ljava/util/Map;)Lorg/icpclive/cds/tunning/TuningRule;
3009+
public final fun fromTeamFields (Ljava/util/Map;)Lorg/icpclive/cds/tunning/TuningRule;
30093010
public final fun listFromInputStream (Ljava/io/InputStream;)Ljava/util/List;
30103011
public final fun listFromString (Ljava/lang/String;)Ljava/util/List;
30113012
public final fun serializer ()Lkotlinx/serialization/KSerializer;

src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/Adapters.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ public fun Flow<ContestUpdate>.addPreviousDays(previousDays: List<ContestState>)
3030
public fun Flow<ContestUpdate>.addPreviousDays(previousDays: List<PreviousDaySettings>): Flow<ContestUpdate> = addPreviousDays(this, previousDays)
3131

3232
public fun Flow<ContestUpdate>.applyTuningRules(tuningRulesFlow: Flow<List<TuningRule>>): Flow<ContestUpdate> = applyTuningRules(this, tuningRulesFlow)
33-
public fun Flow<ContestUpdate>.applyCustomFieldsMap(customFieldsFlow: Flow<Map<TeamId, Map<String, String>>>): Flow<ContestUpdate> = applyCustomFieldsMap(this, customFieldsFlow)
3433

3534
public fun Flow<ContestUpdate>.autoCreateMissingGroupsAndOrgs(): Flow<ContestUpdate> = autoCreateMissingGroupsAndOrgs(this)
3635

src/cds/core/src/main/kotlin/org/icpclive/cds/adapters/impl/CustomFieldsAdapter.kt

Lines changed: 0 additions & 71 deletions
This file was deleted.

src/cds/core/src/main/kotlin/org/icpclive/cds/settings/entrypoint.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public fun CDSSettings.Companion.fromFile(path: Path, credentialProvider: Creden
1313
return when {
1414
!file.exists() -> throw IllegalArgumentException("File ${file.absolutePath} does not exist")
1515
file.name.endsWith(".properties") -> throw IllegalStateException("Properties format is not supported anymore, use settings.json instead")
16+
file.name.endsWith(".json5") -> throw IllegalStateException("Json5 format is not supported anymore, use settings.json instead")
1617
file.name.endsWith(".json") -> {
1718
val format = Json {
1819
serializersModule = serializersModule(credentialProvider, path)

src/cds/core/src/main/kotlin/org/icpclive/cds/tunning/Rules.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,48 @@
11
package org.icpclive.cds.tunning
22

33
import kotlinx.serialization.Serializable
4+
import kotlinx.serialization.descriptors.PrimitiveKind
5+
import kotlinx.serialization.descriptors.SerialDescriptor
6+
import kotlinx.serialization.descriptors.elementDescriptors
7+
import kotlinx.serialization.descriptors.elementNames
48
import kotlinx.serialization.json.Json
9+
import kotlinx.serialization.json.JsonObject
10+
import kotlinx.serialization.json.JsonPrimitive
11+
import kotlinx.serialization.json.buildJsonObject
512
import kotlinx.serialization.json.decodeFromStream
13+
import kotlinx.serialization.serializer
614
import org.icpclive.cds.api.*
715
import org.icpclive.cds.api.AwardsSettings.MedalGroup
816
import java.io.InputStream
917

18+
private fun SerialDescriptor.unwrapInlines(): SerialDescriptor = if (isInline) elementDescriptors.first().unwrapInlines() else this
19+
20+
private inline fun <K, reified T> fieldsToOverrideImpl(rules: Map<K, Map<String, String>>) : Map<K, T> {
21+
val s = serializer<T>()
22+
val knownFields = buildSet {
23+
val descriptor = s.descriptor
24+
for ((index, element) in descriptor.elementNames.withIndex()) {
25+
if (descriptor.getElementDescriptor(index).unwrapInlines().kind == PrimitiveKind.STRING) {
26+
add(element)
27+
}
28+
}
29+
}
30+
return rules.mapValues { (_, data) ->
31+
val knownData = data.filterKeys { it in knownFields }
32+
val customData = data.filterKeys { it !in knownFields }
33+
Json.decodeFromJsonElement(
34+
s,
35+
buildJsonObject {
36+
for ((k, v) in knownData) {
37+
put(k, JsonPrimitive(v))
38+
}
39+
put("customFields", JsonObject(customData.mapValues { JsonPrimitive(it.value) }))
40+
}
41+
)
42+
}
43+
}
44+
45+
1046
/**
1147
* This is a base interface for all rules in advanced.json file.
1248
*
@@ -34,6 +70,13 @@ public sealed interface TuningRule {
3470
public fun tryListFromLegacyFormatFromInputStream(input: InputStream): List<TuningRule>? = runCatching {
3571
AdvancedProperties.fromInputStream(input).toRulesList()
3672
}.getOrNull()
73+
74+
public fun fromTeamFields(input: Map<String, Map<String, String>>): TuningRule {
75+
return OverrideTeams(rules = fieldsToOverrideImpl(input.mapKeys { it.key.toTeamId() }))
76+
}
77+
public fun fromOrganizationFields(input: Map<String, Map<String, String>>): TuningRule {
78+
return OverrideOrganizations(rules = fieldsToOverrideImpl(input.mapKeys { it.key.toOrganizationId() }))
79+
}
3780
}
3881

3982
public fun process(info: ContestInfo): ContestInfo

src/frontend/admin/src/components/ConfigurationEditor.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ enum EditorType {
182182
advancedJson = "advancedJson",
183183
visualConfig = "visualConfig",
184184
customFields = "customFields",
185+
orgCustomFields = "orgCustomFields",
185186
}
186187

187188
function ConfigurationsEditor(): React.ReactElement {
@@ -192,6 +193,9 @@ function ConfigurationsEditor(): React.ReactElement {
192193
console.log(`Selection is now ${event.target.value}`);
193194
setSelection(event.target.value as EditorType);
194195
};
196+
const isCsv =
197+
selection == EditorType.customFields ||
198+
selection == EditorType.orgCustomFields;
195199
return (
196200
<Container>
197201
<Select
@@ -216,17 +220,19 @@ function ConfigurationsEditor(): React.ReactElement {
216220
key={EditorType.customFields}
217221
value={EditorType.customFields}
218222
>
219-
Custom fields
223+
Teams custom fields
224+
</MenuItem>
225+
<MenuItem
226+
key={EditorType.orgCustomFields}
227+
value={EditorType.orgCustomFields}
228+
>
229+
Organizations custom fields
220230
</MenuItem>
221231
</Select>
222232

223233
<ConfigurationEditor
224234
apiRoot={`${BASE_URL_BACKEND}/${selection}`}
225-
editorType={
226-
selection == EditorType.customFields
227-
? EditorLanguage.CSV
228-
: EditorLanguage.Json
229-
}
235+
editorType={isCsv ? EditorLanguage.CSV : EditorLanguage.Json}
230236
/>
231237
</Container>
232238
);

0 commit comments

Comments
 (0)