Merge from master

This commit is contained in:
Claude Brisson
2024-01-23 19:41:13 +01:00
41 changed files with 836 additions and 193 deletions

View File

@@ -67,6 +67,7 @@
</httpConnector> </httpConnector>
<systemProperties> <systemProperties>
<pairgoth.env>${pairgoth.env}</pairgoth.env> <pairgoth.env>${pairgoth.env}</pairgoth.env>
<pairgoth.version>${project.version}</pairgoth.version>
<pairgoth.api.external.url>${pairgoth.api.external.url}</pairgoth.api.external.url> <pairgoth.api.external.url>${pairgoth.api.external.url}</pairgoth.api.external.url>
<pairgoth.webapp.external.url>${pairgoth.webapp.external.url}</pairgoth.webapp.external.url> <pairgoth.webapp.external.url>${pairgoth.webapp.external.url}</pairgoth.webapp.external.url>
<pairgoth.store>${pairgoth.store}</pairgoth.store> <pairgoth.store>${pairgoth.store}</pairgoth.store>

View File

@@ -16,7 +16,7 @@ interface PairgothApiHandler: ApiHandler {
fun Tournament<*>.dispatchEvent(event: Event, data: Json? = null) { fun Tournament<*>.dispatchEvent(event: Event, data: Json? = null) {
Event.dispatch(event, Json.Object("tournament" to id, "data" to data)) Event.dispatch(event, Json.Object("tournament" to id, "data" to data))
// when storage is not in memory, the tournament has to be persisted // when storage is not in memory, the tournament has to be persisted
if (event != Event.tournamentAdded && event != Event.tournamentDeleted && event != Event.gameUpdated) if (event != Event.TournamentAdded && event != Event.TournamentDeleted)
Store.replaceTournament(this) Store.replaceTournament(this)
} }

View File

@@ -7,7 +7,6 @@ import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.getID
import org.jeudego.pairgoth.model.toID import org.jeudego.pairgoth.model.toID
import org.jeudego.pairgoth.model.toJson import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.server.Event
import org.jeudego.pairgoth.server.Event.* import org.jeudego.pairgoth.server.Event.*
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
@@ -21,9 +20,11 @@ object PairingHandler: PairgothApiHandler {
val playing = tournament.games(round).values.flatMap { val playing = tournament.games(round).values.flatMap {
listOf(it.black, it.white) listOf(it.black, it.white)
}.toSet() }.toSet()
val unpairables = tournament.pairables.values.filter { it.skip.contains(round) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray() val unpairables = tournament.pairables.values.filter { !it.final || it.skip.contains(round) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray()
val pairables = tournament.pairables.values.filter { !it.skip.contains(round) && !playing.contains(it.id) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray() val pairables = tournament.pairables.values.filter { it.final && !it.skip.contains(round) && !playing.contains(it.id) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray()
val games = tournament.games(round).values val games = tournament.games(round).values.sortedBy {
if (it.table == 0) Int.MAX_VALUE else it.table
}
return Json.Object( return Json.Object(
"games" to games.map { it.toJson() }.toCollection(Json.MutableArray()), "games" to games.map { it.toJson() }.toCollection(Json.MutableArray()),
"pairables" to pairables, "pairables" to pairables,
@@ -44,19 +45,20 @@ object PairingHandler: PairgothApiHandler {
}.toSet() }.toSet()
val pairables = val pairables =
if (allPlayers) if (allPlayers)
tournament.pairables.values.filter { !it.skip.contains(round) && !playing.contains(it.id) } tournament.pairables.values.filter { it.final && !it.skip.contains(round) && !playing.contains(it.id) }
else payload.map { else payload.map {
// CB - because of the '["all"]' map, conversion to int lands here... Better API syntax for 'all players'? // CB - because of the '["all"]' map, conversion to int lands here... Better API syntax for 'all players'?
if (it is Number) it.toID() else badRequest("invalid pairable id: #$it") if (it is Number) it.toID() else badRequest("invalid pairable id: #$it")
}.map { id -> }.map { id ->
tournament.pairables[id]?.also { tournament.pairables[id]?.also {
if (!it.final) badRequest("pairable #$id registration status is not final")
if (it.skip.contains(round)) badRequest("pairable #$id does not play round $round") if (it.skip.contains(round)) badRequest("pairable #$id does not play round $round")
if (playing.contains(it.id)) badRequest("pairable #$id already plays round $round") if (playing.contains(it.id)) badRequest("pairable #$id already plays round $round")
} ?: badRequest("invalid pairable id: #$id") } ?: badRequest("invalid pairable id: #$id")
} }
val games = tournament.pair(round, pairables) val games = tournament.pair(round, pairables)
val ret = games.map { it.toJson() }.toJsonArray() val ret = games.map { it.toJson() }.toJsonArray()
tournament.dispatchEvent(gamesAdded, Json.Object("round" to round, "games" to ret)) tournament.dispatchEvent(GamesAdded, Json.Object("round" to round, "games" to ret))
return ret return ret
} }
@@ -71,20 +73,37 @@ object PairingHandler: PairgothApiHandler {
val playing = (tournament.games(round).values).filter { it.id != gameId }.flatMap { val playing = (tournament.games(round).values).filter { it.id != gameId }.flatMap {
listOf(it.black, it.white) listOf(it.black, it.white)
}.toSet() }.toSet()
if (game.result != Game.Result.UNKNOWN && (
game.black != payload.getInt("b") ||
game.white != payload.getInt("w") ||
game.handicap != payload.getInt("h")
)) badRequest("Game already has a result")
game.black = payload.getID("b") ?: badRequest("missing black player id") game.black = payload.getID("b") ?: badRequest("missing black player id")
game.white = payload.getID("w") ?: badRequest("missing white player id") game.white = payload.getID("w") ?: badRequest("missing white player id")
tournament.recomputeHdAndDUDD(round, game.id) tournament.recomputeHdAndDUDD(round, game.id)
val previousTable = game.table;
// temporary // temporary
//payload.getInt("dudd")?.let { game.drawnUpDown = it } //payload.getInt("dudd")?.let { game.drawnUpDown = it }
val black = tournament.pairables[game.black] ?: badRequest("invalid black player id") val black = tournament.pairables[game.black] ?: badRequest("invalid black player id")
val white = tournament.pairables[game.black] ?: badRequest("invalid white player id") val white = tournament.pairables[game.black] ?: badRequest("invalid white player id")
if (!black.final) badRequest("black registration status is not final")
if (!white.final) badRequest("white registration status is not final")
if (black.skip.contains(round)) badRequest("black is not playing this round") if (black.skip.contains(round)) badRequest("black is not playing this round")
if (white.skip.contains(round)) badRequest("white is not playing this round") if (white.skip.contains(round)) badRequest("white is not playing this round")
if (playing.contains(black.id)) badRequest("black is already in another game") if (playing.contains(black.id)) badRequest("black is already in another game")
if (playing.contains(white.id)) badRequest("white is already in another game") if (playing.contains(white.id)) badRequest("white is already in another game")
if (payload.containsKey("h")) game.handicap = payload.getString("h")?.toIntOrNull() ?: badRequest("invalid handicap") if (payload.containsKey("h")) game.handicap = payload.getString("h")?.toIntOrNull() ?: badRequest("invalid handicap")
tournament.dispatchEvent(gameUpdated, Json.Object("round" to round, "game" to game.toJson())) if (payload.containsKey("t")) {
game.table = payload.getString("t")?.toIntOrNull() ?: badRequest("invalid table number")
}
tournament.dispatchEvent(GameUpdated, Json.Object("round" to round, "game" to game.toJson()))
if (game.table != previousTable && tournament.renumberTables(round, game)) {
val games = tournament.games(round).values.sortedBy {
if (it.table == 0) Int.MAX_VALUE else it.table
}
tournament.dispatchEvent(TablesRenumbered, Json.Object("round" to round, "games" to games.map { it.toJson() }.toCollection(Json.MutableArray())))
}
return Json.Object("success" to true) return Json.Object("success" to true)
} }
@@ -102,7 +121,7 @@ object PairingHandler: PairgothApiHandler {
payload.forEach { payload.forEach {
val id = (it as Number).toInt() val id = (it as Number).toInt()
val game = tournament.games(round)[id] ?: throw Error("invalid game id") val game = tournament.games(round)[id] ?: throw Error("invalid game id")
if (game.result != Game.Result.UNKNOWN) { if (game.result != Game.Result.UNKNOWN && game.black != 0 && game.white != 0) {
ApiHandler.logger.error("cannot unpair game id ${game.id}: it has a result") ApiHandler.logger.error("cannot unpair game id ${game.id}: it has a result")
// we'll only skip it // we'll only skip it
// throw Error("cannot unpair ") // throw Error("cannot unpair ")
@@ -111,7 +130,7 @@ object PairingHandler: PairgothApiHandler {
} }
} }
} }
tournament.dispatchEvent(gamesDeleted, Json.Object("round" to round, "games" to payload)) tournament.dispatchEvent(GamesDeleted, Json.Object("round" to round, "games" to payload))
return Json.Object("success" to true) return Json.Object("success" to true)
} }
} }

View File

@@ -5,7 +5,6 @@ import com.republicate.kson.toJsonArray
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.Player import org.jeudego.pairgoth.model.Player
import org.jeudego.pairgoth.model.fromJson import org.jeudego.pairgoth.model.fromJson
import org.jeudego.pairgoth.server.Event
import org.jeudego.pairgoth.server.Event.* import org.jeudego.pairgoth.server.Event.*
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
@@ -25,7 +24,7 @@ object PlayerHandler: PairgothApiHandler {
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
val player = Player.fromJson(payload) val player = Player.fromJson(payload)
tournament.players[player.id] = player tournament.players[player.id] = player
tournament.dispatchEvent(playerAdded, player.toJson()) tournament.dispatchEvent(PlayerAdded, player.toJson())
return Json.Object("success" to true, "id" to player.id) return Json.Object("success" to true, "id" to player.id)
} }
@@ -36,7 +35,7 @@ object PlayerHandler: PairgothApiHandler {
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
val updated = Player.fromJson(payload, player) val updated = Player.fromJson(payload, player)
tournament.players[updated.id] = updated tournament.players[updated.id] = updated
tournament.dispatchEvent(playerUpdated, player.toJson()) tournament.dispatchEvent(PlayerUpdated, player.toJson())
return Json.Object("success" to true) return Json.Object("success" to true)
} }
@@ -44,7 +43,7 @@ object PlayerHandler: PairgothApiHandler {
val tournament = getTournament(request) val tournament = getTournament(request)
val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector") val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector")
tournament.players.remove(id) ?: badRequest("invalid player id") tournament.players.remove(id) ?: badRequest("invalid player id")
tournament.dispatchEvent(playerDeleted, Json.Object("id" to id)) tournament.dispatchEvent(PlayerDeleted, Json.Object("id" to id))
return Json.Object("success" to true) return Json.Object("success" to true)
} }
} }

View File

@@ -24,7 +24,7 @@ object ResultsHandler: PairgothApiHandler {
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
val game = tournament.games(round)[payload.getInt("id")] ?: badRequest("invalid game id") val game = tournament.games(round)[payload.getInt("id")] ?: badRequest("invalid game id")
game.result = Game.Result.fromSymbol(payload.getChar("result") ?: badRequest("missing result")) game.result = Game.Result.fromSymbol(payload.getChar("result") ?: badRequest("missing result"))
tournament.dispatchEvent(Event.resultUpdated, Json.Object("round" to round, "data" to game)) tournament.dispatchEvent(Event.ResultUpdated, Json.Object("round" to round, "data" to game))
return Json.Object("success" to true) return Json.Object("success" to true)
} }
} }

View File

@@ -3,21 +3,27 @@ package org.jeudego.pairgoth.api
import com.republicate.kson.Json import com.republicate.kson.Json
import com.republicate.kson.toJsonArray import com.republicate.kson.toJsonArray
import org.jeudego.pairgoth.model.Criterion import org.jeudego.pairgoth.model.Criterion
import org.jeudego.pairgoth.model.Criterion.*
import org.jeudego.pairgoth.model.Game.Result.*
import org.jeudego.pairgoth.model.ID
import org.jeudego.pairgoth.model.MacMahon import org.jeudego.pairgoth.model.MacMahon
import org.jeudego.pairgoth.model.Pairable import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.PairingType import org.jeudego.pairgoth.model.PairingType
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.adjustedTime
import org.jeudego.pairgoth.model.displayRank
import org.jeudego.pairgoth.model.getID
import org.jeudego.pairgoth.model.historyBefore import org.jeudego.pairgoth.model.historyBefore
import org.jeudego.pairgoth.pairing.HistoryHelper import org.jeudego.pairgoth.pairing.HistoryHelper
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
import java.io.PrintWriter
import java.time.format.DateTimeFormatter
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import org.jeudego.pairgoth.model.TimeSystem.TimeSystemType.*
import org.jeudego.pairgoth.model.Criterion.* import java.text.DecimalFormat
import org.jeudego.pairgoth.model.Game.Result.*
import org.jeudego.pairgoth.model.ID
import org.jeudego.pairgoth.model.getID
object StandingsHandler: PairgothApiHandler { object StandingsHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? { override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
@@ -78,12 +84,12 @@ object StandingsHandler: PairgothApiHandler {
DC -> nullMap DC -> nullMap
} }
} }
val pairables = tournament.pairables.values.map { it.toMutableJson() } val pairables = tournament.pairables.values.filter { it.final }.map { it.toMutableJson() }
pairables.forEach { player -> pairables.forEach { player ->
for (crit in criteria) { for (crit in criteria) {
player[crit.first] = crit.second[player.getID()] ?: 0.0 player[crit.first] = crit.second[player.getID()] ?: 0.0
} }
player["results"] = Json.MutableArray(List(round) { "=0" }) player["results"] = Json.MutableArray(List(round) { "0=" })
} }
val sortedPairables = pairables.sortedWith { left, right -> val sortedPairables = pairables.sortedWith { left, right ->
for (crit in criteria) { for (crit in criteria) {
@@ -114,38 +120,156 @@ object StandingsHandler: PairgothApiHandler {
val blackNum = black?.getInt("num") ?: 0 val blackNum = black?.getInt("num") ?: 0
val whiteColor = if (black == null) "" else "w" val whiteColor = if (black == null) "" else "w"
val blackColor = if (white == null) "" else "b" val blackColor = if (white == null) "" else "b"
val handicap = if (game.handicap == 0) "" else "/h${game.handicap}" val handicap = if (game.handicap == 0) "" else "${game.handicap}"
assert(white != null || black != null) assert(white != null || black != null)
if (white != null) { if (white != null) {
val mark = when (game.result) { val mark = when (game.result) {
UNKNOWN -> "?" UNKNOWN -> "?"
BLACK -> "-" BLACK, BOTHLOOSE -> "-"
WHITE -> "+" WHITE, BOTHWIN -> "+"
JIGO -> "=" JIGO, CANCELLED -> "="
CANCELLED -> "X"
BOTHWIN -> "++"
BOTHLOOSE -> "--"
} }
val results = white.getArray("results") as Json.MutableArray val results = white.getArray("results") as Json.MutableArray
results[r - 1] = "$whiteColor$mark$blackNum$handicap" results[r - 1] =
if (blackNum == 0) "0$mark"
else "$blackNum$mark/$whiteColor$handicap"
} }
if (black != null) { if (black != null) {
val mark = when (game.result) { val mark = when (game.result) {
UNKNOWN -> "?" UNKNOWN -> "?"
BLACK -> "+" BLACK, BOTHWIN -> "+"
WHITE -> "-" WHITE, BOTHLOOSE -> "-"
JIGO -> "=" JIGO, CANCELLED -> "="
CANCELLED -> "X"
BOTHWIN -> "++"
BOTHLOOSE -> "--"
} }
val results = black.getArray("results") as Json.MutableArray val results = black.getArray("results") as Json.MutableArray
results[r - 1] = "$blackColor$mark$whiteNum$handicap" results[r - 1] =
if (whiteNum == 0) "0$mark"
else "$whiteNum$mark/$blackColor$handicap"
} }
} }
} }
return sortedPairables.toJsonArray() val accept = request.getHeader("Accept")?.substringBefore(";")
return when(accept) {
"application/json" -> sortedPairables.toJsonArray()
"application/egf" -> {
exportToEGFFormat(tournament, sortedPairables, neededCriteria, response.writer)
return null
}
"application/ffg" -> {
exportToFFGFormat(tournament, sortedPairables, response.writer)
return null
}
else -> ApiHandler.badRequest("invalid Accept header: $accept")
}
} }
val nullMap = mapOf<ID, Double>() val nullMap = mapOf<ID, Double>()
private fun exportToEGFFormat(tournament: Tournament<*>, lines: List<Json.Object>, criteria: List<Criterion>, writer: PrintWriter) {
val mainTime = tournament.timeSystem.mainTime
val adjustedTime = tournament.timeSystem.adjustedTime()
val egfClass =
if (tournament.online) {
when (tournament.timeSystem.type) {
FISCHER ->
if (mainTime >= 1800 && adjustedTime >= 3000) "D"
else "X"
else ->
if (mainTime >= 2400 && adjustedTime >= 3000) "D"
else "X"
}
} else {
when (tournament.timeSystem.type) {
FISCHER ->
if (mainTime >= 2700 && adjustedTime >= 4500) "A"
else if (mainTime >= 1800 && adjustedTime >= 3000) "B"
else if (mainTime >= 1200 && adjustedTime >= 1800) "C"
else "X"
else ->
if (mainTime >= 3600 && adjustedTime >= 4500) "A"
else if (mainTime >= 2400 && adjustedTime >= 3000) "B"
else if (mainTime >= 1500 && adjustedTime >= 1800) "C"
else "X"
}
}
val ret =
"""
; CL[${egfClass}]
; EV[${tournament.name}]
; PC[${tournament.country.lowercase()},${tournament.location}]
; DT[${tournament.startDate},${tournament.endDate}]
; HA[${
if (tournament.pairing.type == PairingType.MAC_MAHON) "h${tournament.pairing.pairingParams.handicap.correction}"
else "h9"
}]
; KM[${tournament.komi}]
; TM[${tournament.timeSystem.adjustedTime() / 60}]
; CM[Generated by Pairgoth v0.1]
;
; Pl Name Rk Co Club ${ criteria.map { it.name.replace(Regex("(S?)O?(SOS|DOS)[MW]?"), "$1$2").padStart(7, ' ') }.joinToString(" ") }
${
lines.joinToString("\n") { player ->
"${
player.getString("num")!!.padStart(4, ' ')
} ${
"${player.getString("name")} ${player.getString("firstname")}".padEnd(30, ' ').take(30)
} ${
displayRank(player.getInt("rank")!!).uppercase().padStart(3, ' ')
} ${
player.getString("country")!!.uppercase()
} ${
(player.getString("club") ?: "").padStart(4).take(4)
} ${
criteria.joinToString(" ") { numFormat.format(player.getDouble(it.name)!!).let { if (it.contains('.')) it else "$it " }.padStart(7, ' ') }
} ${
player.getArray("results")!!.map {
(it as String).padStart(8, ' ')
}.joinToString(" ")
}"
}
}
"""
writer.println(ret)
}
private fun exportToFFGFormat(tournament: Tournament<*>, lines: List<Json.Object>, writer: PrintWriter) {
// let's try in UTF-8
val ret =
""";name=${tournament.shortName}
;date=${frDate.format(tournament.startDate)}
;vill=${tournament.location}${if (tournament.online) "(online)" else ""}
;comm=${tournament.name}
;prog=Pairgoth v0.1
;time=${tournament.timeSystem.mainTime / 60}
;ta=${tournament.timeSystem.adjustedTime() / 60}
;size=${tournament.gobanSize}
;komi=${tournament.komi}
;
;Num Nom Prenom Niv Licence Club
${
lines.joinToString("\n") { player ->
"${
player.getString("num")!!.padStart(4, ' ')
} ${
"${player.getString("name")} ${player.getString("firstname")}".padEnd(24, ' ').take(24)
} ${
displayRank(player.getInt("rank")!!).uppercase().padStart(3, ' ')
} ${
player.getString("ffg") ?: " "
} ${
(player.getString("club") ?: "").padStart(6).take(6)
} ${
player.getArray("results")!!.joinToString(" ") {
(it as String).replace("/", "").replace(Regex("(?<=[bw])$"), "0").padStart(7, ' ')
}
}"
}
}
"""
writer.println(ret)
}
private val numFormat = DecimalFormat("###0.#")
private val frDate: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
} }

View File

@@ -4,7 +4,6 @@ import com.republicate.kson.Json
import com.republicate.kson.toJsonArray import com.republicate.kson.toJsonArray
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.TeamTournament import org.jeudego.pairgoth.model.TeamTournament
import org.jeudego.pairgoth.server.Event
import org.jeudego.pairgoth.server.Event.* import org.jeudego.pairgoth.server.Event.*
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
@@ -26,7 +25,7 @@ object TeamHandler: PairgothApiHandler {
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
val team = tournament.teamFromJson(payload) val team = tournament.teamFromJson(payload)
tournament.teams[team.id] = team tournament.teams[team.id] = team
tournament.dispatchEvent(teamAdded, team.toJson()) tournament.dispatchEvent(TeamAdded, team.toJson())
return Json.Object("success" to true, "id" to team.id) return Json.Object("success" to true, "id" to team.id)
} }
@@ -38,7 +37,7 @@ object TeamHandler: PairgothApiHandler {
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
val updated = tournament.teamFromJson(payload, team) val updated = tournament.teamFromJson(payload, team)
tournament.teams[updated.id] = updated tournament.teams[updated.id] = updated
tournament.dispatchEvent(teamUpdated, team.toJson()) tournament.dispatchEvent(TeamUpdated, team.toJson())
return Json.Object("success" to true) return Json.Object("success" to true)
} }
@@ -47,7 +46,7 @@ object TeamHandler: PairgothApiHandler {
if (tournament !is TeamTournament) badRequest("tournament is not a team tournament") if (tournament !is TeamTournament) badRequest("tournament is not a team tournament")
val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid team selector") val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid team selector")
tournament.teams.remove(id) ?: badRequest("invalid team id") tournament.teams.remove(id) ?: badRequest("invalid team id")
tournament.dispatchEvent(teamDeleted, Json.Object("id" to id)) tournament.dispatchEvent(TeamDeleted, Json.Object("id" to id))
return Json.Object("success" to true) return Json.Object("success" to true)
} }
} }

View File

@@ -11,7 +11,6 @@ import org.jeudego.pairgoth.model.fromJson
import org.jeudego.pairgoth.model.toJson import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.store.Store import org.jeudego.pairgoth.store.Store
import org.jeudego.pairgoth.server.ApiServlet import org.jeudego.pairgoth.server.ApiServlet
import org.jeudego.pairgoth.server.Event
import org.jeudego.pairgoth.server.Event.* import org.jeudego.pairgoth.server.Event.*
import org.w3c.dom.Element import org.w3c.dom.Element
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
@@ -44,7 +43,7 @@ object TournamentHandler: PairgothApiHandler {
else -> badRequest("missing or invalid payload") else -> badRequest("missing or invalid payload")
} }
Store.addTournament(tournament) Store.addTournament(tournament)
tournament.dispatchEvent(tournamentAdded, tournament.toJson()) tournament.dispatchEvent(TournamentAdded, tournament.toJson())
return Json.Object("success" to true, "id" to tournament.id) return Json.Object("success" to true, "id" to tournament.id)
} }
@@ -64,14 +63,14 @@ object TournamentHandler: PairgothApiHandler {
clear() clear()
putAll(tournament.games(round)) putAll(tournament.games(round))
} }
updated.dispatchEvent(tournamentUpdated, updated.toJson()) updated.dispatchEvent(TournamentUpdated, updated.toJson())
return Json.Object("success" to true) return Json.Object("success" to true)
} }
override fun delete(request: HttpServletRequest): Json { override fun delete(request: HttpServletRequest): Json {
val tournament = getTournament(request) val tournament = getTournament(request)
Store.deleteTournament(tournament) Store.deleteTournament(tournament)
tournament.dispatchEvent(tournamentDeleted, Json.Object("id" to tournament.id)) tournament.dispatchEvent(TournamentDeleted, Json.Object("id" to tournament.id))
return Json.Object("success" to true) return Json.Object("success" to true)
} }
} }

View File

@@ -2,7 +2,7 @@ package org.jeudego.pairgoth.ext
import jakarta.xml.bind.JAXBContext import jakarta.xml.bind.JAXBContext
import jakarta.xml.bind.JAXBElement import jakarta.xml.bind.JAXBElement
import kotlinx.datetime.LocalDate import java.time.LocalDate
import org.jeudego.pairgoth.model.* import org.jeudego.pairgoth.model.*
import org.jeudego.pairgoth.opengotha.TournamentType import org.jeudego.pairgoth.opengotha.TournamentType
import org.jeudego.pairgoth.opengotha.ObjectFactory import org.jeudego.pairgoth.opengotha.ObjectFactory
@@ -13,7 +13,7 @@ import javax.xml.datatype.XMLGregorianCalendar
import kotlin.math.roundToInt import kotlin.math.roundToInt
private const val MILLISECONDS_PER_DAY = 86400000 private const val MILLISECONDS_PER_DAY = 86400000
fun XMLGregorianCalendar.toLocalDate() = LocalDate(year, month, day) fun XMLGregorianCalendar.toLocalDate() = LocalDate.of(year, month, day)
object OpenGotha { object OpenGotha {
@@ -114,10 +114,10 @@ object OpenGotha {
location = genParams.location, location = genParams.location,
online = genParams.isBInternet ?: false, online = genParams.isBInternet ?: false,
timeSystem = when (genParams.complementaryTimeSystem) { timeSystem = when (genParams.complementaryTimeSystem) {
"SUDDENDEATH" -> SuddenDeath(genParams.basicTime) "SUDDENDEATH" -> SuddenDeath(genParams.basicTime * 60)
"STDBYOYOMI" -> StandardByoyomi(genParams.basicTime, genParams.stdByoYomiTime, 1) // no periods? "STDBYOYOMI" -> StandardByoyomi(genParams.basicTime * 60, genParams.stdByoYomiTime, 1) // no periods?
"CANBYOYOMI" -> CanadianByoyomi(genParams.basicTime, genParams.canByoYomiTime, genParams.nbMovesCanTime) "CANBYOYOMI" -> CanadianByoyomi(genParams.basicTime * 60, genParams.canByoYomiTime, genParams.nbMovesCanTime)
"FISCHER" -> FischerTime(genParams.basicTime, genParams.fischerTime) "FISCHER" -> FischerTime(genParams.basicTime * 60, genParams.fischerTime)
else -> throw Error("missing byoyomi type") else -> throw Error("missing byoyomi type")
}, },
pairing = when (handParams.hdCeiling) { pairing = when (handParams.hdCeiling) {
@@ -145,7 +145,8 @@ object OpenGotha {
rating = player.rating, rating = player.rating,
rank = Pairable.parseRank(player.rank), rank = Pairable.parseRank(player.rank),
country = player.country, country = player.country,
club = player.club club = player.club,
final = "FIN" == player.registeringStatus
).also { ).also {
player.participating.toString().forEachIndexed { i,c -> player.participating.toString().forEachIndexed { i,c ->
if (c == '0') it.skip.add(i + 1) if (c == '0') it.skip.add(i + 1)
@@ -215,7 +216,9 @@ object OpenGotha {
player.displayRank() player.displayRank()
}" rating="${ }" rating="${
player.rating player.rating
}" ratingOrigin="" registeringStatus="FIN" smmsCorrection="0"/>""" }" ratingOrigin="" registeringStatus="${
if (player.final) "FIN" else "PRE"
}" smmsCorrection="0"/>"""
} }
} }
</Players> </Players>
@@ -269,9 +272,9 @@ object OpenGotha {
} }
</ByePlayer> </ByePlayer>
<TournamentParameterSet> <TournamentParameterSet>
<GeneralParameterSet bInternet="${tournament.online}" basicTime="${tournament.timeSystem.mainTime}" beginDate="${tournament.startDate}" canByoYomiTime="${tournament.timeSystem.byoyomi}" complementaryTimeSystem="${when(tournament.timeSystem.type) { <GeneralParameterSet bInternet="${tournament.online}" basicTime="${tournament.timeSystem.mainTime / 60}" beginDate="${tournament.startDate}" canByoYomiTime="${tournament.timeSystem.byoyomi}" complementaryTimeSystem="${when(tournament.timeSystem.type) {
TimeSystem.TimeSystemType.SUDDEN_DEATH -> "SUDDENDEATH" TimeSystem.TimeSystemType.SUDDEN_DEATH -> "SUDDENDEATH"
TimeSystem.TimeSystemType.STANDARD -> "STDBYOYOMI" TimeSystem.TimeSystemType.JAPANESE -> "STDBYOYOMI"
TimeSystem.TimeSystemType.CANADIAN -> "CANBYOYOMI" TimeSystem.TimeSystemType.CANADIAN -> "CANBYOYOMI"
TimeSystem.TimeSystemType.FISCHER -> "FISCHER" TimeSystem.TimeSystemType.FISCHER -> "FISCHER"
} }" director="" endDate="${tournament.endDate}" fischerTime="${tournament.timeSystem.increment}" genCountNotPlayedGamesAsHalfPoint="false" genMMBar="${ } }" director="" endDate="${tournament.endDate}" fischerTime="${tournament.timeSystem.increment}" genCountNotPlayedGamesAsHalfPoint="false" genMMBar="${

View File

@@ -7,10 +7,10 @@ import java.util.*
// Pairable // Pairable
sealed class Pairable(val id: ID, val name: String, open val rating: Int, open val rank: Int) { sealed class Pairable(val id: ID, val name: String, open val rating: Int, open val rank: Int, val final: Boolean, val mmsCorrection: Int = 0) {
companion object { companion object {
val MIN_RANK: Int = -30 // 30k const val MIN_RANK: Int = -30 // 30k
val MAX_RANK: Int = 8 // 9D const val MAX_RANK: Int = 8 // 9D
} }
abstract fun toJson(): Json.Object abstract fun toJson(): Json.Object
abstract fun toMutableJson(): Json.MutableObject abstract fun toMutableJson(): Json.MutableObject
@@ -26,7 +26,7 @@ sealed class Pairable(val id: ID, val name: String, open val rating: Int, open v
} }
} }
object ByePlayer: Pairable(0, "bye", 0, Int.MIN_VALUE) { object ByePlayer: Pairable(0, "bye", 0, Int.MIN_VALUE, true) {
override fun toJson(): Json.Object { override fun toJson(): Json.Object {
throw Error("bye player should never be serialized") throw Error("bye player should never be serialized")
} }
@@ -70,8 +70,10 @@ class Player(
rating: Int, rating: Int,
rank: Int, rank: Int,
override var country: String, override var country: String,
override var club: String override var club: String,
): Pairable(id, name, rating, rank) { final: Boolean,
mmsCorrection: Int = 0
): Pairable(id, name, rating, rank, final, mmsCorrection) {
companion object companion object
// used to store external IDs ("FFG" => FFG ID, "EGF" => EGF PIN, "AGA" => AGA ID ...) // used to store external IDs ("FFG" => FFG ID, "EGF" => EGF PIN, "AGA" => AGA ID ...)
val externalIds = mutableMapOf<DatabaseId, String>() val externalIds = mutableMapOf<DatabaseId, String>()
@@ -82,9 +84,11 @@ class Player(
"rating" to rating, "rating" to rating,
"rank" to rank, "rank" to rank,
"country" to country, "country" to country,
"club" to club "club" to club,
"final" to final
).also { json -> ).also { json ->
if (skip.isNotEmpty()) json["skip"] = Json.Array(skip) if (skip.isNotEmpty()) json["skip"] = Json.Array(skip)
if (mmsCorrection != 0) json["mmsCorrection"] = mmsCorrection
externalIds.forEach { (dbid, id) -> externalIds.forEach { (dbid, id) ->
json[dbid.key] = id json[dbid.key] = id
} }
@@ -103,7 +107,9 @@ fun Player.Companion.fromJson(json: Json.Object, default: Player? = null) = Play
rating = json.getInt("rating") ?: default?.rating ?: badRequest("missing rating"), rating = json.getInt("rating") ?: default?.rating ?: badRequest("missing rating"),
rank = json.getInt("rank") ?: default?.rank ?: badRequest("missing rank"), rank = json.getInt("rank") ?: default?.rank ?: badRequest("missing rank"),
country = json.getString("country") ?: default?.country ?: badRequest("missing country"), country = json.getString("country") ?: default?.country ?: badRequest("missing country"),
club = json.getString("club") ?: default?.club ?: badRequest("missing club") club = json.getString("club") ?: default?.club ?: badRequest("missing club"),
final = json.getBoolean("final") ?: default?.final ?: true,
mmsCorrection = json.getInt("mmsCorrection") ?: default?.mmsCorrection ?: 0
).also { player -> ).also { player ->
player.skip.clear() player.skip.clear()
json.getArray("skip")?.let { json.getArray("skip")?.let {

View File

@@ -6,6 +6,7 @@ import org.jeudego.pairgoth.model.MainCritParams.SeedMethod.SPLIT_AND_SLIP
import org.jeudego.pairgoth.model.PairingType.* import org.jeudego.pairgoth.model.PairingType.*
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
import org.jeudego.pairgoth.pairing.solver.SwissSolver import org.jeudego.pairgoth.pairing.solver.SwissSolver
import java.util.*
// base pairing parameters // base pairing parameters
data class BaseCritParams( data class BaseCritParams(
@@ -172,7 +173,7 @@ class Swiss(
): Pairing(SWISS, pairingParams, placementParams) { ): Pairing(SWISS, pairingParams, placementParams) {
companion object {} companion object {}
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> { override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
return SwissSolver(round, tournament.historyBefore(round), pairables, pairingParams, placementParams).pair() return SwissSolver(round, tournament.historyBefore(round), pairables, pairingParams, placementParams, tournament.usedTables(round)).pair()
} }
} }
@@ -201,7 +202,7 @@ class MacMahon(
): Pairing(MAC_MAHON, pairingParams, placementParams) { ): Pairing(MAC_MAHON, pairingParams, placementParams) {
companion object {} companion object {}
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> { override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
return MacMahonSolver(round, tournament.historyBefore(round), pairables, pairingParams, placementParams, mmFloor, mmBar).pair() return MacMahonSolver(round, tournament.historyBefore(round), pairables, pairingParams, placementParams, tournament.usedTables(round), mmFloor, mmBar).pair()
} }
} }

View File

@@ -1,7 +1,6 @@
package org.jeudego.pairgoth.model package org.jeudego.pairgoth.model
import com.republicate.kson.Json import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.TimeSystem.TimeSystemType.* import org.jeudego.pairgoth.model.TimeSystem.TimeSystemType.*
@@ -15,7 +14,7 @@ data class TimeSystem(
val stones: Int val stones: Int
) { ) {
companion object {} companion object {}
enum class TimeSystemType { CANADIAN, STANDARD, FISCHER, SUDDEN_DEATH } enum class TimeSystemType { CANADIAN, JAPANESE, FISCHER, SUDDEN_DEATH }
} }
fun CanadianByoyomi(mainTime: Int, byoyomi: Int, stones: Int) = fun CanadianByoyomi(mainTime: Int, byoyomi: Int, stones: Int) =
@@ -30,7 +29,7 @@ fun CanadianByoyomi(mainTime: Int, byoyomi: Int, stones: Int) =
fun StandardByoyomi(mainTime: Int, byoyomi: Int, periods: Int) = fun StandardByoyomi(mainTime: Int, byoyomi: Int, periods: Int) =
TimeSystem( TimeSystem(
type = STANDARD, type = JAPANESE,
mainTime = mainTime, mainTime = mainTime,
increment = 0, increment = 0,
byoyomi = byoyomi, byoyomi = byoyomi,
@@ -86,9 +85,16 @@ fun TimeSystem.Companion.fromJson(json: Json.Object) =
fun TimeSystem.toJson() = when (type) { fun TimeSystem.toJson() = when (type) {
TimeSystem.TimeSystemType.CANADIAN -> Json.Object("type" to type.name, "mainTime" to mainTime, "byoyomi" to byoyomi, "stones" to stones) TimeSystem.TimeSystemType.CANADIAN -> Json.Object("type" to type.name, "mainTime" to mainTime, "byoyomi" to byoyomi, "stones" to stones)
TimeSystem.TimeSystemType.STANDARD -> Json.Object("type" to type.name, "mainTime" to mainTime, "byoyomi" to byoyomi, "periods" to periods) TimeSystem.TimeSystemType.JAPANESE -> Json.Object("type" to type.name, "mainTime" to mainTime, "byoyomi" to byoyomi, "periods" to periods)
TimeSystem.TimeSystemType.FISCHER -> TimeSystem.TimeSystemType.FISCHER ->
if (maxTime == Int.MAX_VALUE) Json.Object("type" to type.name, "mainTime" to mainTime, "increment" to increment) if (maxTime == Int.MAX_VALUE) Json.Object("type" to type.name, "mainTime" to mainTime, "increment" to increment)
else Json.Object("type" to type.name, "mainTime" to mainTime, "increment" to increment, "maxTime" to maxTime) else Json.Object("type" to type.name, "mainTime" to mainTime, "increment" to increment, "maxTime" to maxTime)
TimeSystem.TimeSystemType.SUDDEN_DEATH -> Json.Object("type" to type.name, "mainTime" to mainTime) TimeSystem.TimeSystemType.SUDDEN_DEATH -> Json.Object("type" to type.name, "mainTime" to mainTime)
} }
fun TimeSystem.adjustedTime() = when (type) {
TimeSystem.TimeSystemType.CANADIAN -> mainTime + 60 * byoyomi / stones
TimeSystem.TimeSystemType.JAPANESE -> mainTime + 45 * byoyomi
TimeSystem.TimeSystemType.FISCHER -> mainTime + 120 * increment
TimeSystem.TimeSystemType.SUDDEN_DEATH -> mainTime
}

View File

@@ -2,7 +2,9 @@ package org.jeudego.pairgoth.model
import com.republicate.kson.Json import com.republicate.kson.Json
import com.republicate.kson.toJsonArray import com.republicate.kson.toJsonArray
import kotlinx.datetime.LocalDate // CB TODO - review
//import kotlinx.datetime.LocalDate
import java.time.LocalDate
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.pairing.HistoryHelper import org.jeudego.pairgoth.pairing.HistoryHelper
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
@@ -10,6 +12,7 @@ import org.jeudego.pairgoth.pairing.solver.SwissSolver
import org.jeudego.pairgoth.store.Store import org.jeudego.pairgoth.store.Store
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
sealed class Tournament <P: Pairable>( sealed class Tournament <P: Pairable>(
@@ -76,9 +79,9 @@ sealed class Tournament <P: Pairable>(
// TODO cleaner solver instantiation // TODO cleaner solver instantiation
val history = historyBefore(round) val history = historyBefore(round)
val solver = if (pairing is Swiss) { val solver = if (pairing is Swiss) {
SwissSolver(round, history, pairables.values.toList(), pairing.pairingParams, pairing.placementParams) SwissSolver(round, history, pairables.values.toList(), pairing.pairingParams, pairing.placementParams, usedTables(round))
} else if (pairing is MacMahon) { } else if (pairing is MacMahon) {
MacMahonSolver(round, history, pairables.values.toList(), pairing.pairingParams, pairing.placementParams, pairing.mmFloor, pairing.mmBar) MacMahonSolver(round, history, pairables.values.toList(), pairing.pairingParams, pairing.placementParams, usedTables(round), pairing.mmFloor, pairing.mmBar)
} else throw Exception("Invalid tournament type") } else throw Exception("Invalid tournament type")
// Recomputes DUDD and hd // Recomputes DUDD and hd
@@ -88,6 +91,29 @@ sealed class Tournament <P: Pairable>(
game.drawnUpDown = solver.dudd(black, white) game.drawnUpDown = solver.dudd(black, white)
game.handicap = solver.hd(black, white) game.handicap = solver.hd(black, white)
} }
fun usedTables(round: Int): BitSet =
games(round).values.map { it.table }.fold(BitSet()) { acc, table ->
acc.set(table)
acc
}
fun renumberTables(round: Int, pivot: Game? = null): Boolean {
var changed = false
var nextTable = 1
games(round).values.filter{ game -> pivot?.let { pivot.id != game.id } ?: true }.sortedBy { game ->
val whiteRank = pairables[game.white]?.rating ?: Int.MIN_VALUE
val blackRank = pairables[game.black]?.rating ?: Int.MIN_VALUE
-(2 * whiteRank + 2 * blackRank) / 2
}.forEach { game ->
if (pivot != null && nextTable == pivot.table) {
++nextTable
}
changed = changed || game.table != nextTable
game.table = nextTable++
}
return changed
}
} }
// standard tournament of individuals // standard tournament of individuals
@@ -133,7 +159,7 @@ class TeamTournament(
override val players = mutableMapOf<ID, Player>() override val players = mutableMapOf<ID, Player>()
val teams: MutableMap<ID, Team> = _pairables val teams: MutableMap<ID, Team> = _pairables
inner class Team(id: ID, name: String): Pairable(id, name, 0, 0) { inner class Team(id: ID, name: String, final: Boolean): Pairable(id, name, 0, 0, final) {
val playerIds = mutableSetOf<ID>() val playerIds = mutableSetOf<ID>()
val teamPlayers: Set<Player> get() = playerIds.mapNotNull { players[id] }.toSet() val teamPlayers: Set<Player> get() = playerIds.mapNotNull { players[id] }.toSet()
override val rating: Int get() = if (teamPlayers.isEmpty()) super.rating else (teamPlayers.sumOf { player -> player.rating.toDouble() } / players.size).roundToInt() override val rating: Int get() = if (teamPlayers.isEmpty()) super.rating else (teamPlayers.sumOf { player -> player.rating.toDouble() } / players.size).roundToInt()
@@ -151,7 +177,8 @@ class TeamTournament(
fun teamFromJson(json: Json.Object, default: TeamTournament.Team? = null) = Team( fun teamFromJson(json: Json.Object, default: TeamTournament.Team? = null) = Team(
id = json.getInt("id") ?: default?.id ?: Store.nextPlayerId, id = json.getInt("id") ?: default?.id ?: Store.nextPlayerId,
name = json.getString("name") ?: default?.name ?: badRequest("missing name") name = json.getString("name") ?: default?.name ?: badRequest("missing name"),
final = json.getBoolean("final") ?: default?.final ?: badRequest("missing final")
).apply { ).apply {
json.getArray("players")?.let { arr -> json.getArray("players")?.let { arr ->
arr.mapTo(playerIds) { arr.mapTo(playerIds) {
@@ -174,8 +201,8 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n
type = type, type = type,
name = json.getString("name") ?: default?.name ?: badRequest("missing name"), name = json.getString("name") ?: default?.name ?: badRequest("missing name"),
shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"), shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"),
startDate = json.getLocalDate("startDate") ?: default?.startDate ?: badRequest("missing startDate"), startDate = json.getString("startDate")?.let { LocalDate.parse(it) } ?: default?.startDate ?: badRequest("missing startDate"),
endDate = json.getLocalDate("endDate") ?: default?.endDate ?: badRequest("missing endDate"), endDate = json.getString("endDate")?.let { LocalDate.parse(it) } ?: default?.endDate ?: badRequest("missing endDate"),
country = json.getString("country") ?: default?.country ?: badRequest("missing country"), country = json.getString("country") ?: default?.country ?: badRequest("missing country"),
location = json.getString("location") ?: default?.location ?: badRequest("missing location"), location = json.getString("location") ?: default?.location ?: badRequest("missing location"),
online = json.getBoolean("online") ?: default?.online ?: false, online = json.getBoolean("online") ?: default?.online ?: false,
@@ -192,8 +219,8 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n
type = type, type = type,
name = json.getString("name") ?: default?.name ?: badRequest("missing name"), name = json.getString("name") ?: default?.name ?: badRequest("missing name"),
shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"), shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"),
startDate = json.getLocalDate("startDate") ?: default?.startDate ?: badRequest("missing startDate"), startDate = json.getString("startDate")?.let { LocalDate.parse(it) } ?: default?.startDate ?: badRequest("missing startDate"),
endDate = json.getLocalDate("endDate") ?: default?.endDate ?: badRequest("missing endDate"), endDate = json.getString("endDate")?.let { LocalDate.parse(it) } ?: default?.endDate ?: badRequest("missing endDate"),
country = json.getString("country") ?: default?.country ?: badRequest("missing country"), country = json.getString("country") ?: default?.country ?: badRequest("missing country"),
location = json.getString("location") ?: default?.location ?: badRequest("missing location"), location = json.getString("location") ?: default?.location ?: badRequest("missing location"),
online = json.getBoolean("online") ?: default?.online ?: false, online = json.getBoolean("online") ?: default?.online ?: false,

View File

@@ -136,11 +136,4 @@ abstract class BasePairingHelper(
open fun nameSort(p: Pairable, q: Pairable): Int { open fun nameSort(p: Pairable, q: Pairable): Int {
return if (p.name > q.name) 1 else -1 return if (p.name > q.name) 1 else -1
} }
val tables = history.mapTo(mutableListOf()) { games ->
games.map { it.table }.fold(BitSet()) { acc, table ->
acc.set(table)
acc
}
}
} }

View File

@@ -24,6 +24,7 @@ sealed class BaseSolver(
pairables: List<Pairable>, // All pairables for this round, it may include the bye player pairables: List<Pairable>, // All pairables for this round, it may include the bye player
pairing: PairingParams, pairing: PairingParams,
placement: PlacementParams, placement: PlacementParams,
val usedTables: BitSet
) : BasePairingHelper(history, pairables, pairing, placement) { ) : BasePairingHelper(history, pairables, pairing, placement) {
companion object { companion object {
@@ -540,7 +541,6 @@ sealed class BaseSolver(
} }
open fun games(black: Pairable, white: Pairable): List<Game> { open fun games(black: Pairable, white: Pairable): List<Game> {
// CB TODO team of individuals pairing // CB TODO team of individuals pairing
val usedTables = tables.getOrNull(round - 1) ?: BitSet().also { tables.add(it) }
val table = if (black.id == 0 || white.id == 0) 0 else usedTables.nextClearBit(1) val table = if (black.id == 0 || white.id == 0) 0 else usedTables.nextClearBit(1)
usedTables.set(table) usedTables.set(table)
return listOf(Game(id = Store.nextGameId, table = table, black = black.id, white = white.id, handicap = hd(white, black), drawnUpDown = dudd(black, white))) return listOf(Game(id = Store.nextGameId, table = table, black = black.id, white = white.id, handicap = hd(white, black), drawnUpDown = dudd(black, white)))

View File

@@ -1,6 +1,7 @@
package org.jeudego.pairgoth.pairing.solver package org.jeudego.pairgoth.pairing.solver
import org.jeudego.pairgoth.model.* import org.jeudego.pairgoth.model.*
import java.util.*
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -9,9 +10,9 @@ class MacMahonSolver(round: Int,
pairables: List<Pairable>, pairables: List<Pairable>,
pairingParams: PairingParams, pairingParams: PairingParams,
placementParams: PlacementParams, placementParams: PlacementParams,
usedTables: BitSet,
private val mmFloor: Int, private val mmBar: Int): private val mmFloor: Int, private val mmBar: Int):
BaseSolver(round, history, pairables, pairingParams, placementParams) { BaseSolver(round, history, pairables, pairingParams, placementParams, usedTables) {
override val scores: Map<ID, Double> by lazy { override val scores: Map<ID, Double> by lazy {
require (mmBar > mmFloor) { "MMFloor is higher than MMBar" } require (mmBar > mmFloor) { "MMFloor is higher than MMBar" }
@@ -32,7 +33,7 @@ class MacMahonSolver(round: Int,
} }
} }
val Pairable.mmBase: Double get() = min(max(rank, mmFloor), mmBar) + mmsZero val Pairable.mmBase: Double get() = min(max(rank, mmFloor), mmBar) + mmsZero + mmsCorrection
val Pairable.mms: Double get() = scores[id] ?: 0.0 val Pairable.mms: Double get() = scores[id] ?: 0.0
// CB TODO - configurable criteria // CB TODO - configurable criteria

View File

@@ -1,13 +1,16 @@
package org.jeudego.pairgoth.pairing.solver package org.jeudego.pairgoth.pairing.solver
import org.jeudego.pairgoth.model.* import org.jeudego.pairgoth.model.*
import java.util.*
class SwissSolver(round: Int, class SwissSolver(round: Int,
history: List<List<Game>>, history: List<List<Game>>,
pairables: List<Pairable>, pairables: List<Pairable>,
pairingParams: PairingParams, pairingParams: PairingParams,
placementParams: PlacementParams): placementParams: PlacementParams,
BaseSolver(round, history, pairables, pairingParams, placementParams) { usedTables: BitSet
):
BaseSolver(round, history, pairables, pairingParams, placementParams, usedTables) {
// In a Swiss tournament the main criterion is the number of wins and already computed // In a Swiss tournament the main criterion is the number of wins and already computed

View File

@@ -218,8 +218,12 @@ class ApiServlet: HttpServlet() {
"Missing 'Accept' header" "Missing 'Accept' header"
) )
// CB TODO 1) a reference to a specific API call at this point is a code smell. // CB TODO 1) a reference to a specific API call at this point is a code smell.
// 2) there will e other content types: .tou, .h9, .html // 2) there will be other content types: .tou, .h9, .html
if (!isJson(accept) && (!isXml(accept) || !request.requestURI.matches(Regex("/api/tour/\\d+")))) throw ApiException( if (!isJson(accept) &&
(!isXml(accept) || !request.requestURI.matches(Regex("/api/tour/\\d+"))) &&
(accept != "application/ffg" && accept != "application/egf" || !request.requestURI.matches(Regex("/api/tour/\\d+/standings/\\d+")))
) throw ApiException(
HttpServletResponse.SC_BAD_REQUEST, HttpServletResponse.SC_BAD_REQUEST,
"Invalid 'Accept' header" "Invalid 'Accept' header"
) )

View File

@@ -4,19 +4,20 @@ import info.macias.sse.events.MessageEvent
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
enum class Event { enum class Event {
tournamentAdded, TournamentAdded,
tournamentUpdated, TournamentUpdated,
tournamentDeleted, TournamentDeleted,
playerAdded, PlayerAdded,
playerUpdated, PlayerUpdated,
playerDeleted, PlayerDeleted,
teamAdded, TeamAdded,
teamUpdated, TeamUpdated,
teamDeleted, TeamDeleted,
gamesAdded, GamesAdded,
gamesDeleted, GamesDeleted,
gameUpdated, GameUpdated,
resultUpdated, ResultUpdated,
TablesRenumbered
; ;
companion object { companion object {

3
version.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
mvn versions:set -DnewVersion=$1

View File

@@ -153,11 +153,6 @@
<artifactId>kotlin-reflect</artifactId> <artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version> <version>${kotlin.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-datetime-jvm</artifactId>
<version>0.4.0</version>
</dependency>
<!-- servlets and mail APIs --> <!-- servlets and mail APIs -->
<dependency> <dependency>
<groupId>jakarta.servlet</groupId> <groupId>jakarta.servlet</groupId>

View File

@@ -25,6 +25,8 @@ object FFGRatingsHandler: RatingsHandler(RatingsManager.Ratings.FFG) {
val rating = it["rating"]?.toString()?.toIntOrNull() val rating = it["rating"]?.toString()?.toIntOrNull()
if (rating != null) { if (rating != null) {
it["rank"] = (rating/100).let { if (it < 0) "${-it}k" else "${it+1}d" } it["rank"] = (rating/100).let { if (it < 0) "${-it}k" else "${it+1}d" }
// then adjust to match EGF ratings
it["rating"] = rating + 2050
} }
} }
} }

View File

@@ -9,6 +9,8 @@ import com.republicate.kson.Json
class PairgothTool { class PairgothTool {
fun toMap(array: Json.Array) = array.map { ser -> ser as Json.Object }.associateBy { it.getLong("id")!! } fun toMap(array: Json.Array) = array.map { ser -> ser as Json.Object }.associateBy { it.getLong("id")!! }
fun countFinals(array: Json.Array) = array.map { ser -> ser as Json.Object }.count { it.getBoolean("final") ?: false }
fun getCriteria() = mapOf( fun getCriteria() = mapOf(
"NONE" to "No tie break", // No ranking / tie-break "NONE" to "No tie break", // No ranking / tie-break
@@ -43,4 +45,17 @@ class PairgothTool {
"SDC" to "Simplified direct confrontation", // Simplified direct confrontation "SDC" to "Simplified direct confrontation", // Simplified direct confrontation
"DC" to "Direct confrontation", // Direct confrontation "DC" to "Direct confrontation", // Direct confrontation
) )
fun getResultsStats(games: Collection<Json.Object>): Json.Object {
var total = 0
var known = 0
games
.filter{ it.getInt("b")!! != 0 && it.getInt("w")!! != 0 }
.map { it -> it.getString("r") }
.forEach {
++total
if ("?" != it) ++known
}
return Json.Object("total" to total, "known" to known)
}
} }

View File

@@ -210,6 +210,24 @@
text-align: center; text-align: center;
} }
.ui.form .field :invalid {
color: #9f3a38;
background: #fff6f6;
background-image: initial;
background-position-x: initial;
background-position-y: initial;
background-size: initial;
background-repeat-x: initial;
background-repeat-y: initial;
background-attachment: initial;
background-origin: initial;
background-clip: initial;
}
.ui.striped.table>tbody>tr:nth-child(2n),.ui.striped.table>tr:nth-child(2n) {
background-color: rgba(0,0,50,.1)
}
.form-actions { .form-actions {
position: sticky; position: sticky;
bottom: 1em; bottom: 1em;
@@ -359,7 +377,7 @@
cursor: pointer; cursor: pointer;
} }
thead { thead th {
position: sticky; position: sticky;
top: 0; top: 0;
} }
@@ -371,4 +389,46 @@
a.disabled { a.disabled {
color: darkgray; color: darkgray;
} }
@media print {
body {
width: unset;
height: unset;
font-size: 0.65em;
}
.roundbox {
border: none;
}
#title {
font-size: 1rem !important;
margin-top: 0.1em !important;
}
#logo, #lang, .steps, #filter-box, #footer, #pairing-left, #pairing-buttons, button, #standings-params, #logout {
display: none !important;
}
.circular.label {
transform: scale(0.7);
}
#pairing-right {
max-width: unset !important;
}
#paired {
max-height: unset !important;
max-width: unset !important;
font-size: 1rem !important;
line-height: 1.1rem !important;
min-width: 60vw;
&::before {
top: 0;
}
}
}
} }

View File

@@ -54,6 +54,18 @@
/* registration section */ /* registration section */
#list-header {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
margin: 0 1em;
}
#players-list {
max-width: 95vw;
overflow-x: auto;
}
#player { #player {
&.create { &.create {
.edition { .edition {
@@ -138,6 +150,35 @@
#player.popup { #player.popup {
min-width: 65vw; min-width: 65vw;
#final-reg {
.final {
color: green;
display: none;
}
&.final {
.preliminary {
display: none;
}
.final {
display: initial;
}
}
}
}
td.reg-status {
.final {
color: green;
display: none;
}
&.final {
.final {
display: initial;
}
.preliminary {
display: none;
}
}
} }
/* pairing section */ /* pairing section */
@@ -195,8 +236,30 @@
background-color: rgba(100,200,255,200); background-color: rgba(100,200,255,200);
cursor: grab; cursor: grab;
} }
&:not(.selected):nth-child(2n) {
background-color: rgba(0,0,50,.1)
} }
} }
}
#pairables {
margin-bottom: 1em;
}
#paired {
.listitem {
position: relative;
gap: 0;
.table, .handicap {
width: 3em;
}
.black, .white {
width: 45%;
}
.levels {
width: 6em;
}
}
}
#pairing-buttons { #pairing-buttons {
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
@@ -280,4 +343,8 @@
justify-content: space-around; justify-content: space-around;
} }
} }
#standings-container {
max-width: 95vw;
overflow-x: auto;
}
} }

View File

@@ -145,4 +145,6 @@ the configuration guide le guide de configuration
to à to à
unpairable players joueurs non disponibles unpairable players joueurs non disponibles
version 0.1 supports the version 0.1 supporte le système dappariement version 0.1 supports the version 0.1 supporte le système dappariement
white blanc
white vs. black blanc vs. Noir white vs. black blanc vs. Noir
confirmed. confirmé(s).

View File

@@ -9,13 +9,16 @@ const apiVersion = '1.0';
// .catch(err => { ... }); // .catch(err => { ... });
const base = '/api/'; const base = '/api/';
let headers = function() { let headers = function(withJson) {
let ret = { let ret = {
"Content-Type": "application/json; charset=utf-8", 'Accept-Version': apiVersion,
"Accept-Version": apiVersion, 'Accept': 'application/json',
"Accept": "application/json", 'X-Browser-Key': store('browserKey')
"X-Browser-Key": store('browserKey')
}; };
if (typeof(withJson) === 'undefined') withJson = true;
if (withJson) {
ret['Content-Type'] = 'application/json';
}
let accessToken = store('accessToken'); let accessToken = store('accessToken');
if (accessToken) { if (accessToken) {
ret['Authorization'] = `Bearer ${accessToken}`; ret['Authorization'] = `Bearer ${accessToken}`;

View File

@@ -120,9 +120,15 @@ Element.prototype.hide = function() {
return this; return this;
} }
NodeList.prototype.text = function(txt) { NodeList.prototype.text = function(txt) {
this.item(0).text(txt); if (typeof(txt) === 'undefined') {
return this.item(0).text();
} else {
this.forEach(elem => {
elem.text(txt);
});
return this; return this;
} }
}
Element.prototype.text = function(txt) { Element.prototype.text = function(txt) {
if (typeof(txt) === 'undefined') { if (typeof(txt) === 'undefined') {
return this.textContent; return this.textContent;

View File

@@ -161,6 +161,7 @@ function modal(id) {
function close_modal() { function close_modal() {
$('body').removeClass('dimmed'); $('body').removeClass('dimmed');
$(`.popup`).removeClass('shown'); $(`.popup`).removeClass('shown');
store('addingPlayers', false);
} }
function downloadFile(blob, filename) { function downloadFile(blob, filename) {
@@ -189,17 +190,23 @@ onLoad(() => {
// keyboard handling // keyboard handling
document.on('keyup', e => { document.on('keyup', e => {
let tab = document.location.hash;
switch (e.key) { switch (e.key) {
case 'Escape': { case 'Escape': {
if (tab === '#registration') {
if ($('#player').hasClass('shown') && $('#needle')[0].value) { if ($('#player').hasClass('shown') && $('#needle')[0].value) {
$('#needle')[0].value = ''; $('#needle')[0].value = '';
initSearch(); initSearch();
} else { } else {
close_modal(); close_modal();
} }
} else if (tab === '#pairing') {
$('#pairing-lists .selected.listitem').removeClass('selected');
}
break; break;
} }
case 'ArrowDown': { case 'ArrowDown': {
if (tab === '#registration') {
if (typeof(searchResultShown) === 'function' && searchResultShown()) { if (typeof(searchResultShown) === 'function' && searchResultShown()) {
let lines = $('.result-line'); let lines = $('.result-line');
if (typeof (searchHighlight) === 'undefined') searchHighlight = 0; if (typeof (searchHighlight) === 'undefined') searchHighlight = 0;
@@ -208,9 +215,11 @@ onLoad(() => {
lines.removeClass('highlighted'); lines.removeClass('highlighted');
lines[searchHighlight].addClass('highlighted'); lines[searchHighlight].addClass('highlighted');
} }
}
break; break;
} }
case 'ArrowUp': { case 'ArrowUp': {
if (tab === '#registration') {
if (typeof(searchResultShown) === 'function' && searchResultShown()) { if (typeof(searchResultShown) === 'function' && searchResultShown()) {
let lines = $('.result-line'); let lines = $('.result-line');
if (typeof (searchHighlight) === 'undefined') searchHighlight = 0; if (typeof (searchHighlight) === 'undefined') searchHighlight = 0;
@@ -219,9 +228,11 @@ onLoad(() => {
lines.removeClass('highlighted'); lines.removeClass('highlighted');
lines[searchHighlight].addClass('highlighted'); lines[searchHighlight].addClass('highlighted');
} }
}
break; break;
} }
case 'Enter': { case 'Enter': {
if (tab === '#registration') {
if (typeof(searchResultShown) === 'function') { if (typeof(searchResultShown) === 'function') {
if (searchResultShown()) { if (searchResultShown()) {
fillPlayer(searchResult[searchHighlight]); fillPlayer(searchResult[searchHighlight]);
@@ -229,6 +240,7 @@ onLoad(() => {
$('#register')[0].click(); $('#register')[0].click();
} }
} }
}
break; break;
} }
} }
@@ -241,5 +253,18 @@ onLoad(() => {
}, 1); }, 1);
} }
// persistent scroll
$('#center').on('scroll', e => {
let scroll = $('#center')[0].scrollTop;
store('scroll', scroll);
});
let persistentScroll = store('scroll');
if (persistentScroll) {
setTimeout(() => {
$('#center')[0].scrollTop = persistentScroll;
let scroll = $('#center')[0].scrollTop;
}, 200);
}
}); });

View File

@@ -118,7 +118,6 @@ onLoad(() => {
$('#tournament-infos').on('submit', e => { $('#tournament-infos').on('submit', e => {
e.preventDefault(); e.preventDefault();
let form = e.target; let form = e.target;
console.log(form.val('country'));
let tour = { let tour = {
name: form.val('name'), name: form.val('name'),
shortName: form.val('shortName'), shortName: form.val('shortName'),

View File

@@ -18,6 +18,26 @@ function unpair(games) {
}); });
} }
function editGame(game) {
let t = game.find('.table');
let w = game.find('.white');
let b = game.find('.black');
let h = game.find('.handicap');
let form = $('#pairing-form')[0];
form.val('id', game.data('id'));
form.val('t', t.data('value'));
form.val('w', w.data('id'));
$('#edit-pairing-white').text(w.text());
form.val('b', b.data('id'));
$('#edit-pairing-black').text(b.text());
form.val('h', h.data('value'));
$('#update-pairing').addClass('disabled');
modal('edit-pairing');
}
onLoad(()=>{ onLoad(()=>{
$('.listitem').on('click', e => { $('.listitem').on('click', e => {
if (e.shiftKey && typeof(focused) !== 'undefined') { if (e.shiftKey && typeof(focused) !== 'undefined') {
@@ -31,13 +51,17 @@ onLoad(()=>{
let parent = e.target.closest('.multi-select'); let parent = e.target.closest('.multi-select');
let children = parent.childNodes.filter('.listitem'); let children = parent.childNodes.filter('.listitem');
for (let j = from; j <= to; ++j) { new Tablesort($('#players')[0]); for (let j = from; j <= to; ++j) { new Tablesort($('#players')[0]);
children.item(j).addClass('selected'); children.item(j).addClass('selected');
children.item(j).attr('draggable', true); children.item(j).attr('draggable', true);
} }
} else { } else {
let target = e.target.closest('.listitem'); let target = e.target.closest('.listitem');
if (e.detail === 1) {
focused = target.toggleClass('selected').attr('draggable', target.hasClass('selected')); focused = target.toggleClass('selected').attr('draggable', target.hasClass('selected'));
} else {
focused = target.attr('draggable', target.hasClass('selected'));
editGame(focused);
}
} }
}); });
$('#pair').on('click', e => { $('#pair').on('click', e => {
@@ -56,4 +80,39 @@ onLoad(()=>{
} }
unpair(games); unpair(games);
}); });
$('#pairing-form [name]').on('input', e => {
$('#update-pairing').removeClass('disabled');
});
$('#pairing-exchange').on('click', e => {
let form = $('#pairing-form')[0];
let w = form.val('w');
let b = form.val('b');
form.val('w', b);
form.val('b', w);
let wName = $('#edit-pairing-white').text();
let bName = $('#edit-pairing-black').text();
$('#edit-pairing-white').text(bName);
$('#edit-pairing-black').text(wName);
$('#update-pairing').removeClass('disabled');
});
$('#pairing-form').on('submit', e => {
e.preventDefault();
return false;
});
$('#update-pairing').on('click', e => {
let form = $('#pairing-form')[0];
let game = {
id: form.val('id'),
t: form.val('t'),
w: form.val('w'),
b: form.val('b'),
h: form.val('h')
}
api.putJson(`tour/${tour_id}/pair/${activeRound}`, game)
.then(game => {
if (game !== 'error') {
document.location.reload();
}
});
});
}); });

View File

@@ -67,18 +67,45 @@ function parseRank(rank) {
} }
function fillPlayer(player) { function fillPlayer(player) {
// hack UK / GB
let country = player.country.toLowerCase();
if ('uk' === country) country = 'gb';
let form = $('#player-form')[0]; let form = $('#player-form')[0];
form.val('name', player.name); form.val('name', player.name);
form.val('firstname', player.firstname); form.val('firstname', player.firstname);
form.val('country', player.country.toLowerCase()); console.log(country);
form.val('country', country);
form.val('club', player.club); form.val('club', player.club);
form.val('rank', parseRank(player.rank)); form.val('rank', parseRank(player.rank));
form.val('rating', player.rating); form.val('rating', player.rating);
form.val('final', false);
$('#needle')[0].value = ''; $('#needle')[0].value = '';
initSearch(); initSearch();
$('#register').focus(); $('#register').focus();
} }
function addPlayers() {
let form = $('#player-form')[0];
form.addClass('add');
// keep preliminary/final status
let status = form.val('final') || false;
form.reset();
// initial search checkboxes position
['countryFilter', 'aga', 'egf', 'ffg'].forEach(id => {
let value = store(id);
if (value !== null && typeof(value) !== 'undefined') {
$(`#${id}`)[0].checked = value;
}
});
form.val('final', status);
$('#player').removeClass('edit').addClass('create');
modal('player');
$('#needle').focus();
store('addingPlayers', true);
}
let tableSort;
onLoad(() => { onLoad(() => {
$('input.numeric').imask({ $('input.numeric').imask({
mask: Number, mask: Number,
@@ -86,15 +113,39 @@ onLoad(() => {
min: 0, min: 0,
max: 4000 max: 4000
}); });
new Tablesort($('#players')[0]);
let prevSort = store('registrationSort');
if (prevSort) {
let columns = $('#players thead th');
columns.forEach(th => {
th.removeAttribute('data-sort-default');
th.removeAttribute('aria-sort');
})
prevSort.forEach(i => {
let col = columns[Math.abs(i)];
col.setAttribute('data-sort-default', '1');
if (i < 0) {
// take into account TableSort initiailization bug
col.setAttribute('aria-sort', 'ascending');
}
});
}
tableSort = new Tablesort($('#players')[0]);
$('#players').on('afterSort', e => {
let sort = [];
$('#players thead th').forEach((th, i) => {
let attr = th.attr('aria-sort');
if (attr) {
let dir = i;
if (attr === 'descending') dir = -dir;
sort.push(dir);
}
});
store('registrationSort', sort);
});
$('#add').on('click', e => { $('#add').on('click', e => {
let form = $('#player-form')[0]; addPlayers();
form.addClass('add');
// $('#player-form input.participation').forEach(chk => chk.checked = true);
form.reset();
$('#player').removeClass('edit').addClass('create');
modal('player');
$('#needle').focus();
}); });
$('#cancel-register').on('click', e => { $('#cancel-register').on('click', e => {
e.preventDefault(); e.preventDefault();
@@ -122,7 +173,6 @@ onLoad(() => {
$('#player-form')[0].dispatchEvent(new CustomEvent('submit', {cancelable: true})); $('#player-form')[0].dispatchEvent(new CustomEvent('submit', {cancelable: true}));
}); });
$('#player-form').on('submit', e => { $('#player-form').on('submit', e => {
("submitting!!")
e.preventDefault(); e.preventDefault();
let form = $('#player-form')[0]; let form = $('#player-form')[0];
let player = { let player = {
@@ -132,11 +182,13 @@ onLoad(() => {
rank: form.val('rank'), rank: form.val('rank'),
country: form.val('country'), country: form.val('country'),
club: form.val('club'), club: form.val('club'),
skip: form.find('input.participation').map((input,i) => [i+1, input.checked]).filter(arr => !arr[1]).map(arr => arr[0]) skip: form.find('input.participation').map((input,i) => [i+1, input.checked]).filter(arr => !arr[1]).map(arr => arr[0]),
final: form.val('final')
} }
if (form.hasClass('add')) { if (form.hasClass('add')) {
api.postJson(`tour/${tour_id}/part`, player) api.postJson(`tour/${tour_id}/part`, player)
.then(player => { .then(player => {
console.log(player)
if (player !== 'error') { if (player !== 'error') {
window.location.reload(); window.location.reload();
} }
@@ -146,6 +198,7 @@ onLoad(() => {
player['id'] = id; player['id'] = id;
api.putJson(`tour/${tour_id}/part/${id}`, player) api.putJson(`tour/${tour_id}/part/${id}`, player)
.then(player => { .then(player => {
console.log(player)
if (player !== 'error') { if (player !== 'error') {
window.location.reload(); window.location.reload();
} }
@@ -153,6 +206,8 @@ onLoad(() => {
} }
}); });
$('#players > tbody > tr').on('click', e => { $('#players > tbody > tr').on('click', e => {
let regStatus = e.target.closest('td.reg-status');
if (regStatus) return;
let id = e.target.closest('tr').attr('data-id'); let id = e.target.closest('tr').attr('data-id');
api.getJson(`tour/${tour_id}/part/${id}`) api.getJson(`tour/${tour_id}/part/${id}`)
.then(player => { .then(player => {
@@ -163,8 +218,11 @@ onLoad(() => {
form.val('firstname', player.firstname); form.val('firstname', player.firstname);
form.val('rating', player.rating); form.val('rating', player.rating);
form.val('rank', player.rank); form.val('rank', player.rank);
form.val('country', player.country); form.val('country', player.country.toLowerCase());
form.val('club', player.club); form.val('club', player.club);
form.val('final', player.final);
if (player.final) $('#final-reg').addClass('final');
else $('#final-reg').removeClass('final');
for (r = 1; r <= tour_rounds; ++r) { for (r = 1; r <= tour_rounds; ++r) {
form.val(`r${r}`, !(player.skip && player.skip.includes(r))); form.val(`r${r}`, !(player.skip && player.skip.includes(r)));
} }
@@ -192,6 +250,9 @@ onLoad(() => {
let chk = e.target.closest('.toggle'); let chk = e.target.closest('.toggle');
let checkbox = chk.find('input')[0]; let checkbox = chk.find('input')[0];
checkbox.checked = !checkbox.checked; checkbox.checked = !checkbox.checked;
let id = checkbox.getAttribute('id');
let value = checkbox.checked;
store(id, value);
initSearch(); initSearch();
}); });
document.on('click', e => { document.on('click', e => {
@@ -211,4 +272,49 @@ onLoad(() => {
} }
}); });
}); });
$('#reg-status').on('click', e => {
let current = $('#final-reg').hasClass('final');
if (current) {
$('input[name="final"]')[0].value = false;
$('#final-reg').removeClass('final');
} else {
$('input[name="final"]')[0].value = true;
$('#final-reg').addClass('final');
}
});
$('.reg-status').on('click', e => {
let cell = e.target.closest('td');
let tr = e.target.closest('tr');
let id = tr.data('id');
let newStatus = !cell.hasClass('final');
api.putJson(`tour/${tour_id}/part/${id}`, {
id: id,
final: newStatus
}).then(player => {
if (player !== 'error') {
cell.toggleClass('final');
standingsUpToDate = false;
pairablesUpToDate = false;
}
});
e.preventDefault();
return false;
});
$('#filter').on('input', (e) => {
let input = e.target;
let value = input.value.toUpperCase();
if (value === '') $('tbody > tr').removeClass('hidden');
else $('tbody > tr').forEach(tr => {
let txt = tr.data('text');
if (txt && txt.indexOf(value) === -1) tr.addClass('hidden');
else tr.removeClass('hidden');
});
});
$('#filter-box i').on('click', e => {
$('#filter')[0].value = '';
$('tbody > tr').removeClass('hidden');
});
if (store('addingPlayers')) {
addPlayers();
}
}); });

View File

@@ -1,4 +1,4 @@
function setResult(id, result) { function setResult(id, result, previous) {
api.putJson(`tour/${tour_id}/res/${activeRound}`, { id: id, result: result }) api.putJson(`tour/${tour_id}/res/${activeRound}`, { id: id, result: result })
.then(res => { .then(res => {
if (res !== 'error') { if (res !== 'error') {
@@ -9,15 +9,26 @@ function setResult(id, result) {
let dispResult = result; let dispResult = result;
switch (result) { switch (result) {
case '?': break; case '?': break;
case 'w': white.addClass('winner'); black.addClass('looser'); dispResult = 'w+'; break; case 'w': white.addClass('winner'); black.addClass('looser'); dispResult = '1-0'; break;
case 'b': black.addClass('winner'); white.addClass('looser'); dispResult = 'b+'; break; case 'b': black.addClass('winner'); white.addClass('looser'); dispResult = '0-1'; break;
case '=': break; case '=': dispResult = '½-½'; break;
case 'X': break; case 'X': break;
case '#': white.addClass('winner'); black.addClass('winner'); dispResult = '1-1'; break; case '#': white.addClass('winner'); black.addClass('winner'); dispResult = '1-1'; break;
case '0': white.addClass('looser'); black.addClass('looser'); dispResult = '0-0'; break; case '0': white.addClass('looser'); black.addClass('looser'); dispResult = '0-0'; break;
} }
let resultCell = row.find('td.result'); let resultCell = row.find('td.result');
resultCell.text(dispResult).data('result', result); resultCell.text(dispResult).data('result', result);
standingsUpToDate = false;
if (previous === '?') {
let indicator = $('#known')[0];
let known = parseInt(indicator.innerText);
indicator.innerText = ++known;
} else if (result === '?') {
let indicator = $('#known')[0];
let known = parseInt(indicator.innerText);
indicator.innerText = --known;
}
} }
}) })
} }
@@ -35,11 +46,9 @@ onLoad(()=>{
$('#results-table .result').on('click', e => { $('#results-table .result').on('click', e => {
let cell = e.target.closest('.result'); let cell = e.target.closest('.result');
let gameId = e.target.closest('tr').data('id'); let gameId = e.target.closest('tr').data('id');
let result = cell.data('result'); let oldResult = cell.data('result');
let index = results.indexOf(result); let index = results.indexOf(oldResult);
console.log(index) let newResult = results[(index + 1)%results.length];
result = results[(index + 1)%results.length]; setResult(gameId, newResult, oldResult);
console.log(result)
setResult(gameId, result);
}); });
}); });

View File

@@ -1,3 +1,20 @@
function publish(format, extension) {
let form = $('#tournament-infos')[0];
let shortName = form.val('shortName');
let hdrs = headers();
hdrs['Accept'] = `application/${format}`
fetch(`api/tour/${tour_id}/standings/${activeRound}`, {
headers: hdrs
}).then(resp => {
if (resp.ok) return resp.text()
else throw "publish error"
}).then(txt => {
let blob = new Blob(['\uFEFF', txt.trim()], {type: 'plain/text;charset=utf-8'});
downloadFile(blob, `${shortName}.${extension}`);
close_modal();
}).catch(err => showError(err));
}
onLoad(() => { onLoad(() => {
$('.criterium').on('click', e => { $('.criterium').on('click', e => {
let alreadyOpen = e.target.closest('select'); let alreadyOpen = e.target.closest('select');
@@ -45,4 +62,10 @@ onLoad(() => {
$('#publish-modal').on('click', e => { $('#publish-modal').on('click', e => {
close_modal(); close_modal();
}); });
$('.publish-ffg').on('click', e => {
publish('ffg', 'tou');
});
$('.publish-egf').on('click', e => {
publish('egf', 'h9');
});
}); });

View File

@@ -1,6 +1 @@
/*! (function(){function e(t,n){if(!(this instanceof e))return new e(t,n);if(!t||t.tagName!=="TABLE")throw new Error("Element must be a table");this.init(t,n||{})}var t=[],n=function(e){var t;return!window.CustomEvent||typeof window.CustomEvent!="function"?(t=document.createEvent("CustomEvent"),t.initCustomEvent(e,!1,!1,void 0)):t=new CustomEvent(e),t},s=function(e,t){return e.getAttribute(t.sortAttribute||"data-sort")||e.textContent||e.innerText||""},a=function(e,t){return e=e.trim().toLowerCase(),t=t.trim().toLowerCase(),e===t?0:e<t?1:-1},o=function(e,t){return[].slice.call(e).find(function(e){return e.getAttribute("data-sort-column-key")===t})},i=function(e,t){return function(n,s){var o=e(n.td,s.td);return o===0?t?s.index-n.index:n.index-s.index:o}};e.extend=function(e,n,s){if(typeof n!="function"||typeof s!="function")throw new Error("Pattern and sort must be a function");t.push({name:e,pattern:n,sort:s})},e.prototype={init:function(e,t){var s,o,i,a,r,n=this;if(n.table=e,n.thead=!1,n.options=t,e.rows&&e.rows.length>0)if(e.tHead&&e.tHead.rows.length>0){for(s=0;s<e.tHead.rows.length;s++)if(e.tHead.rows[s].getAttribute("data-sort-method")==="thead"){o=e.tHead.rows[s];break}o||(o=e.tHead.rows[e.tHead.rows.length-1]),n.thead=!0}else o=e.rows[0];if(!o)return;r=function(){n.current&&n.current!==this&&n.current.removeAttribute("aria-sort"),n.current=this,n.sortTable(this)};for(s=0;s<o.cells.length;s++)i=o.cells[s],i.setAttribute("role","columnheader"),i.getAttribute("data-sort-method")!=="none"&&(i.tabindex=0,i.addEventListener("click",r,!1),i.getAttribute("data-sort-default")!==null&&(a=i));a&&(n.current=a,n.sortTable(a))},sortTable:function(e,r){var h,l=this,v=e.getAttribute("data-sort-column-key"),_=e.cellIndex,g=a,c="",p=[],d=l.thead?0:1,j=e.getAttribute("data-sort-method"),m=e.getAttribute("aria-sort");if(l.table.dispatchEvent(n("beforeSort")),r||(m==="ascending"?m="descending":m==="descending"?m="ascending":m=l.options.descending?"descending":"ascending",e.setAttribute("aria-sort",m)),l.table.rows.length<2)return;if(!j){for(;p.length<3&&d<l.table.tBodies[0].rows.length;)v?h=o(l.table.tBodies[0].rows[d].cells,v):h=l.table.tBodies[0].rows[d].cells[_],c=h?s(h,l.options):"",c=c.trim(),c.length>0&&p.push(c),d++;if(!p)return}for(d=0;d<t.length;d++)if(c=t[d],j){if(c.name===j){g=c.sort;break}}else if(p.every(c.pattern)){g=c.sort;break}l.col=_;for(d=0;d<l.table.tBodies.length;d++){var u,f=[],y={},b=0,w=0;if(l.table.tBodies[d].rows.length<2)continue;for(u=0;u<l.table.tBodies[d].rows.length;u++)c=l.table.tBodies[d].rows[u],c.getAttribute("data-sort-method")==="none"?y[b]=c:(v?h=o(c.cells,v):h=c.cells[l.col],f.push({tr:c,td:h?s(h,l.options):"",index:b})),b++;m==="descending"?f.sort(i(g,!0)):(f.sort(i(g,!1)),f.reverse());for(u=0;u<b;u++)y[u]?(c=y[u],w++):c=f[u-w].tr,l.table.tBodies[d].appendChild(c)}l.table.dispatchEvent(n("afterSort"))},refresh:function(){this.current!==void 0&&this.sortTable(this.current,!0)}},typeof module!="undefined"&&module.exports?module.exports=e:window.Tablesort=e})()
* tablesort v5.4.0 (2023-05-04)
* http://tristen.ca/tablesort/demo/
* Copyright (c) 2023 ; Licensed MIT
*/
!function(){function r(t,e){if(!(this instanceof r))return new r(t,e);if(!t||"TABLE"!==t.tagName)throw new Error("Element must be a table");this.init(t,e||{})}function m(t){var e;return window.CustomEvent&&"function"==typeof window.CustomEvent?e=new CustomEvent(t):(e=document.createEvent("CustomEvent")).initCustomEvent(t,!1,!1,void 0),e}function p(t,e){return t.getAttribute(e.sortAttribute||"data-sort")||t.textContent||t.innerText||""}function v(t,e){return(t=t.trim().toLowerCase())===(e=e.trim().toLowerCase())?0:t<e?1:-1}function A(t,e){return[].slice.call(t).find(function(t){return t.getAttribute("data-sort-column-key")===e})}function E(n,o){return function(t,e){var r=n(t.td,e.td);return 0===r?o?e.index-t.index:t.index-e.index:r}}var x=[];r.extend=function(t,e,r){if("function"!=typeof e||"function"!=typeof r)throw new Error("Pattern and sort must be a function");x.push({name:t,pattern:e,sort:r})},r.prototype={init:function(t,e){var r,n,o,i=this;if(i.table=t,i.thead=!1,i.options=e,t.rows&&0<t.rows.length)if(t.tHead&&0<t.tHead.rows.length){for(a=0;a<t.tHead.rows.length;a++)if("thead"===t.tHead.rows[a].getAttribute("data-sort-method")){r=t.tHead.rows[a];break}r=r||t.tHead.rows[t.tHead.rows.length-1],i.thead=!0}else r=t.rows[0];if(r){function s(){i.current&&i.current!==this&&i.current.removeAttribute("aria-sort"),i.current=this,i.sortTable(this)}for(var a=0;a<r.cells.length;a++)(o=r.cells[a]).setAttribute("role","columnheader"),"none"!==o.getAttribute("data-sort-method")&&(o.tabindex=0,o.addEventListener("click",s,!1),null!==o.getAttribute("data-sort-default")&&(n=o));n&&(i.current=n,i.sortTable(n))}},sortTable:function(t,e){var r=this,n=t.getAttribute("data-sort-column-key"),o=t.cellIndex,i=v,s="",a=[],d=r.thead?0:1,u=t.getAttribute("data-sort-method"),l=t.getAttribute("aria-sort");if(r.table.dispatchEvent(m("beforeSort")),e||(l="ascending"===l||"descending"!==l&&r.options.descending?"descending":"ascending",t.setAttribute("aria-sort",l)),!(r.table.rows.length<2)){if(!u){for(;a.length<3&&d<r.table.tBodies[0].rows.length;)0<(s=(s=(f=n?A(r.table.tBodies[0].rows[d].cells,n):r.table.tBodies[0].rows[d].cells[o])?p(f,r.options):"").trim()).length&&a.push(s),d++;if(!a)return}for(d=0;d<x.length;d++)if(s=x[d],u){if(s.name===u){i=s.sort;break}}else if(a.every(s.pattern)){i=s.sort;break}for(r.col=o,d=0;d<r.table.tBodies.length;d++){var c,f,h=[],b={},w=0,g=0;if(!(r.table.tBodies[d].rows.length<2)){for(c=0;c<r.table.tBodies[d].rows.length;c++)"none"===(s=r.table.tBodies[d].rows[c]).getAttribute("data-sort-method")?b[w]=s:(f=n?A(s.cells,n):s.cells[r.col],h.push({tr:s,td:f?p(f,r.options):"",index:w})),w++;for("descending"===l?h.sort(E(i,!0)):(h.sort(E(i,!1)),h.reverse()),c=0;c<w;c++)b[c]?(s=b[c],g++):s=h[c-g].tr,r.table.tBodies[d].appendChild(s)}}r.table.dispatchEvent(m("afterSort"))}},refresh:function(){void 0!==this.current&&this.sortTable(this.current,!0)}},"undefined"!=typeof module&&module.exports?module.exports=r:window.Tablesort=r}();

View File

@@ -51,10 +51,65 @@
#foreach($game in $games) #foreach($game in $games)
#set($white = $pmap[$game.w]) #set($white = $pmap[$game.w])
#set($black = $pmap[$game.b]) #set($black = $pmap[$game.b])
<div class="listitem game" data-id="$game.id"><span class="table">#$game.t</span><span class="white">#if($white)$white.name $white.firstname #rank($white.rank)#{else}BIP#end</span><span>&nbsp;</span><span class="black">#if($black)$black.name $black.firstname #rank($black.rank)#{else}BIP#end</span>#if($game.h)<span class="handicap">h$game.h</span>#end</div> <div class="listitem game" data-id="$game.id">
<div class="table" data-value="$game.t">#$game.t</div>
<div class="white" data-id="$game.w">#if($white)$white.name $white.firstname#{else}BIP#end</div>
<div class="levels">#if($white)#rank($white.rank)#end&nbsp;/&nbsp;#if($black)#rank($black.rank)#end</div>
<div class="black" data-id="$game.b">#if($black)$black.name $black.firstname#{else}BIP#end</div>
<div class="handicap" data-value="$game.h">#if($game.h)h$game.h#{else}&nbsp;#end</div>
</div>
#end #end
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="edit-pairing" class="popup">
<div class="popup-body">
<form id="pairing-form" class="ui form edit">
<input type="hidden" name="id"/>
<div class="popup-content">
<div class="inline fields">
<div class="field">
<label>Tbl</label>
<span class="nobreak">#<input name="t" type="number"/></span>
</div>
<div class="field">
<label>White</label>
<input type="hidden" name="w"/>
<div id="edit-pairing-white"></div>
</div>
<div class="field">
<button id="pairing-exchange" type="button" class="ui icon button">
<i class="fa fa-exchange"></i>
</button>
</div>
<div class="field">
<label>Black</label>
<input type="hidden" name="b"/>
<div id="edit-pairing-black"></div>
</div>
<div class="field">
<label>Hd</label>
<select name="h">
#foreach($h in [0..9])
<option value="$h">$h</option>
#end
</select>
</div>
</div>
</div>
<div class="popup-footer">
<button id="cancel-pairing" type="button" class="ui gray right labeled icon floating close button">
<i class="times icon"></i>
Cancel
</button>
<button id="update-pairing" type="button" class="ui green right labeled icon floating button">
<i class="check icon"></i>
Update
</button>
</div>
</form>
</div>
</div>

View File

@@ -1,24 +1,40 @@
<div class="tab-content" id="registration-tab">
<div id="reg-view">
<div id="players-list" class="roundbox">
#set($parts = $api.get("tour/${params.id}/part")) #set($parts = $api.get("tour/${params.id}/part"))
#set($pmap = $utils.toMap($parts)) #set($pmap = $utils.toMap($parts))
<div class="tab-content" id="registration-tab">
<div id="reg-view">
<div id="list-header">
<div id="filter-box" class="ui icon input">
<input type="text" id="filter" placeholder="Search..."/>
<i class="circular times link icon"></i>
</div>
<div>
$parts.size() participants, $utils.countFinals($parts) confirmed.
</div>
</div>
<div id="players-list" class="roundbox">
<table id="players" class="ui celled selectable striped table"> <table id="players" class="ui celled selectable striped table">
<thead> <thead>
<th>Reg</th>
<th>Name</th> <th>Name</th>
<th>First name</th> <th>First name</th>
<th>Country</th> <th>Country</th>
<th>Club</th> <th>Club</th>
<th>Rank</th> <th>Rank</th>
<th>Rating</th> ## TableSort bug which inverts specified sort...
<th>Participation</th> <th data-sort-default="1" aria-sort="ascending">Rating</th>
<th data-sort-method="none">Participation</th>
</thead> </thead>
<tbody> <tbody>
#foreach($part in $parts) #foreach($part in $parts)
<tr data-id="$part.id"> <tr data-id="$part.id" data-text="$esc.html("$part.name.toUpperCase() $part.firstname.toUpperCase() $part.club.toUpperCase()")">
<td class="centered reg-status #if($part.final)final#end">
<span class="preliminary"><i class="fa fa-question"></i></span>
<span class="final"><i class="fa fa-check"></i></span>
</td>
<td>$part.name</td> <td>$part.name</td>
<td>$part.firstname</td> <td>$part.firstname</td>
<td>$part.country</td> <td>$part.country.toUpperCase()</td>
<td>$part.club</td> <td>$part.club</td>
<td data-sort="$part.rank">#rank($part.rank)</td> <td data-sort="$part.rank">#rank($part.rank)</td>
<td>$part.rating</td> <td>$part.rating</td>
@@ -141,6 +157,14 @@
</div> </div>
</div> </div>
<div class="inline fields"> <div class="inline fields">
<label>Final ?</label>
<div id="final-reg" class="field">
<input name="final" type="hidden"/>
<button id="reg-status" type="button" class="ui icon roundedremove mini button">
<span class="final"><i class="fa fa-check"></i></span>
<span class="preliminary"><i class="fa fa-question"></i></span>
</button>
</div>
<label>Participation</label> <label>Participation</label>
#foreach($r in [1..$tour.rounds]) #foreach($r in [1..$tour.rounds])
<div class="centered field"> <div class="centered field">
@@ -154,7 +178,7 @@
<button id="cancel-register" type="button" class="ui gray right labeled icon floating close button"> <button id="cancel-register" type="button" class="ui gray right labeled icon floating close button">
<i class="times icon"></i> <i class="times icon"></i>
<span class="edition">Close</span> <span class="edition">Close</span>
<span class="creation">Cancel</span> <span class="creation">Close</span>
</button> </button>
<button id="unregister" type="button" class="ui red right labeled icon floating button"> <button id="unregister" type="button" class="ui red right labeled icon floating button">
<i class="trash icon"></i> <i class="trash icon"></i>

View File

@@ -4,6 +4,8 @@
<button class="ui floating choose-round prev-round button">&laquo;</button> <button class="ui floating choose-round prev-round button">&laquo;</button>
<span class="active-round">$round</span> <span class="active-round">$round</span>
<button class="ui floating choose-round next-round button">&raquo;</button> <button class="ui floating choose-round next-round button">&raquo;</button>
#set($stats = $utils.getResultsStats($games))
<span class="norbeak"><span id="known">$stats.known</span> / $stats.total</span>
</div> </div>
<div id="results-list" class="roundbox"> <div id="results-list" class="roundbox">
<table id="results-table" class="ui celled striped table"> <table id="results-table" class="ui celled striped table">
@@ -14,7 +16,7 @@
<th>result</th> <th>result</th>
</thead> </thead>
<tbody> <tbody>
#set($dispRst = {'?':'?', 'w':'w+', 'b':'b+', '=':'=', 'X':'X', '#':'1-1', '0':'0-0'}) #set($dispRst = {'?':'?', 'w':'1-0', 'b':'0-1', '=':'½-½', 'X':'X', '#':'1-1', '0':'0-0'})
#foreach($game in $games) #foreach($game in $games)
#set($white = $pmap[$game.w]) #set($white = $pmap[$game.w])
#set($black = $pmap[$game.b]) #set($black = $pmap[$game.b])
@@ -23,7 +25,7 @@
<td>#$game.t</td> <td>#$game.t</td>
<td class="white player #if($game.r == 'w' || $game.r == '#') winner #elseif($game.r == 'b' || $game.r == '0') looser #end" data-id="$white.id" data-sort="$white.name $white.firstname"><span>#if($white)$white.name $white.firstname #rank($white.rank)#{else}BIP#end</span></td> <td class="white player #if($game.r == 'w' || $game.r == '#') winner #elseif($game.r == 'b' || $game.r == '0') looser #end" data-id="$white.id" data-sort="$white.name $white.firstname"><span>#if($white)$white.name $white.firstname #rank($white.rank)#{else}BIP#end</span></td>
<td class="black player #if($game.r == 'b' || $game.r == '#') winner #elseif($game.r == 'w' || $game.r == '0') looser #end" data-id="$black.id" data-sort="$black.name $black.firstname"><span>#if($black)$black.name $black.firstname #rank($black.rank)#{else}BIP#end</span></td> <td class="black player #if($game.r == 'b' || $game.r == '#') winner #elseif($game.r == 'w' || $game.r == '0') looser #end" data-id="$black.id" data-sort="$black.name $black.firstname"><span>#if($black)$black.name $black.firstname #rank($black.rank)#{else}BIP#end</span></td>
<td class="result centered" data-result="$game.r">$dispRst[$game.r]</td> <td class="result centered" data-sort="$game.r" data-result="$game.r">$dispRst[$game.r]</td>
</tr> </tr>
#end #end
#end #end

View File

@@ -66,7 +66,7 @@
<td>$part.num</td> <td>$part.num</td>
<td>$part.place</td> <td>$part.place</td>
<td>$part.name $part.firstname</td> <td>$part.name $part.firstname</td>
<td>#rank($part.rank)</td> <td data-sort="$part.rank">#rank($part.rank)</td>
<td>$part.country</td> <td>$part.country</td>
<td>$number.format('0.#', $part.NBW)</td> <td>$number.format('0.#', $part.NBW)</td>
#set($mx = $round - 1) #set($mx = $round - 1)
@@ -101,9 +101,9 @@
</div> </div>
<div class="popup-footer"> <div class="popup-footer">
<div class="form-actions"> <div class="form-actions">
<button class="ui gray floating cancel button">Cancel</button> <button type="button" class="ui gray floating cancel button">Cancel</button>
<button class="ui blue floating button">EGF</button> <button type="button" class="ui blue floating publish-egf button">EGF</button>
<button class="ui blue floating button">FFG</button> <button type="button" class="ui blue floating publish-ffg button">FFG</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -73,6 +73,8 @@
const tour_id = ${tour.id}; const tour_id = ${tour.id};
const tour_rounds = ${tour.rounds}; const tour_rounds = ${tour.rounds};
let activeRound = ${round}; let activeRound = ${round};
let standingsUpToDate = true;
let pairablesUpToDate = true;
// $params // $params
#end #end
#set($datepickerLocale = $translate.datepickerLocale($request.lang, $request.loc)) #set($datepickerLocale = $translate.datepickerLocale($request.lang, $request.loc))
@@ -105,6 +107,11 @@
$('.step').removeClass('active'); $('.step').removeClass('active');
$(`.step[data-step="${step}"], #${step}-tab`).addClass('active'); $(`.step[data-step="${step}"], #${step}-tab`).addClass('active');
window.location.hash = `#${step}`; window.location.hash = `#${step}`;
if (step === 'standings' && !standingsUpToDate) {
window.location.reload();
} else if (step === 'pairing' && !pairablesUpToDate) {
window.location.reload();
}
} }
onLoad(() => { onLoad(() => {