diff --git a/api-webapp/pom.xml b/api-webapp/pom.xml index 6e528d2..25752af 100644 --- a/api-webapp/pom.xml +++ b/api-webapp/pom.xml @@ -67,6 +67,7 @@ ${pairgoth.env} + ${project.version} ${pairgoth.api.external.url} ${pairgoth.webapp.external.url} ${pairgoth.store} diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt index c903d17..891d240 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt @@ -13,7 +13,7 @@ interface ApiHandler { when (request.method) { "GET" -> get(request, response) "POST" -> post(request) - "PUT" -> put(request) + "PUT" -> put(request) "DELETE" -> delete(request) else -> notImplemented() } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairgothApiHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairgothApiHandler.kt index 9c4e41f..f28139c 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairgothApiHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairgothApiHandler.kt @@ -16,7 +16,7 @@ interface PairgothApiHandler: ApiHandler { fun Tournament<*>.dispatchEvent(event: Event, data: Json? = null) { Event.dispatch(event, Json.Object("tournament" to id, "data" to data)) // 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) } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt index b668cc4..e28db24 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt @@ -7,7 +7,6 @@ import org.jeudego.pairgoth.model.Game import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.toID import org.jeudego.pairgoth.model.toJson -import org.jeudego.pairgoth.server.Event import org.jeudego.pairgoth.server.Event.* import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse @@ -21,9 +20,11 @@ object PairingHandler: PairgothApiHandler { val playing = tournament.games(round).values.flatMap { listOf(it.black, it.white) }.toSet() - val unpairables = tournament.pairables.values.filter { 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 games = tournament.games(round).values + 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.final && !it.skip.contains(round) && !playing.contains(it.id) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray() + val games = tournament.games(round).values.sortedBy { + if (it.table == 0) Int.MAX_VALUE else it.table + } return Json.Object( "games" to games.map { it.toJson() }.toCollection(Json.MutableArray()), "pairables" to pairables, @@ -44,19 +45,20 @@ object PairingHandler: PairgothApiHandler { }.toSet() val pairables = 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 { // 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") }.map { id -> 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 (playing.contains(it.id)) badRequest("pairable #$id already plays round $round") } ?: badRequest("invalid pairable id: #$id") } val games = tournament.pair(round, pairables) 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 } @@ -71,20 +73,37 @@ object PairingHandler: PairgothApiHandler { val playing = (tournament.games(round).values).filter { it.id != gameId }.flatMap { listOf(it.black, it.white) }.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.white = payload.getID("w") ?: badRequest("missing white player id") tournament.recomputeHdAndDUDD(round, game.id) + val previousTable = game.table; // temporary //payload.getInt("dudd")?.let { game.drawnUpDown = it } val black = tournament.pairables[game.black] ?: badRequest("invalid black 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 (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(white.id)) badRequest("white is already in another game") 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) } @@ -102,7 +121,7 @@ object PairingHandler: PairgothApiHandler { payload.forEach { val id = (it as Number).toInt() 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") // we'll only skip it // 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) } } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt index 5c0795b..1780b4d 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt @@ -5,7 +5,6 @@ import com.republicate.kson.toJsonArray import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.model.Player import org.jeudego.pairgoth.model.fromJson -import org.jeudego.pairgoth.server.Event import org.jeudego.pairgoth.server.Event.* import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse @@ -25,7 +24,7 @@ object PlayerHandler: PairgothApiHandler { val payload = getObjectPayload(request) val player = Player.fromJson(payload) 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) } @@ -36,7 +35,7 @@ object PlayerHandler: PairgothApiHandler { val payload = getObjectPayload(request) val updated = Player.fromJson(payload, player) tournament.players[updated.id] = updated - tournament.dispatchEvent(playerUpdated, player.toJson()) + tournament.dispatchEvent(PlayerUpdated, player.toJson()) return Json.Object("success" to true) } @@ -44,7 +43,7 @@ object PlayerHandler: PairgothApiHandler { val tournament = getTournament(request) val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector") 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) } } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt index 372fdc6..a87b06d 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt @@ -24,7 +24,7 @@ object ResultsHandler: PairgothApiHandler { val payload = getObjectPayload(request) val game = tournament.games(round)[payload.getInt("id")] ?: badRequest("invalid game id") 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) } } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/StandingsHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/StandingsHandler.kt index b664685..e4413c7 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/StandingsHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/StandingsHandler.kt @@ -3,21 +3,27 @@ package org.jeudego.pairgoth.api import com.republicate.kson.Json import com.republicate.kson.toJsonArray 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.Pairable 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.pairing.HistoryHelper 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.HttpServletResponse import kotlin.math.max import kotlin.math.min - -import org.jeudego.pairgoth.model.Criterion.* -import org.jeudego.pairgoth.model.Game.Result.* -import org.jeudego.pairgoth.model.ID -import org.jeudego.pairgoth.model.getID +import org.jeudego.pairgoth.model.TimeSystem.TimeSystemType.* +import java.text.DecimalFormat object StandingsHandler: PairgothApiHandler { override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? { @@ -78,12 +84,12 @@ object StandingsHandler: PairgothApiHandler { DC -> nullMap } } - val pairables = tournament.pairables.values.map { it.toMutableJson() } + val pairables = tournament.pairables.values.filter { it.final }.map { it.toMutableJson() } pairables.forEach { player -> for (crit in criteria) { 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 -> for (crit in criteria) { @@ -114,38 +120,156 @@ object StandingsHandler: PairgothApiHandler { val blackNum = black?.getInt("num") ?: 0 val whiteColor = if (black == null) "" else "w" 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) if (white != null) { val mark = when (game.result) { UNKNOWN -> "?" - BLACK -> "-" - WHITE -> "+" - JIGO -> "=" - CANCELLED -> "X" - BOTHWIN -> "++" - BOTHLOOSE -> "--" + BLACK, BOTHLOOSE -> "-" + WHITE, BOTHWIN -> "+" + JIGO, CANCELLED -> "=" } 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) { val mark = when (game.result) { UNKNOWN -> "?" - BLACK -> "+" - WHITE -> "-" - JIGO -> "=" - CANCELLED -> "X" - BOTHWIN -> "++" - BOTHLOOSE -> "--" + BLACK, BOTHWIN -> "+" + WHITE, BOTHLOOSE -> "-" + JIGO, CANCELLED -> "=" } 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() + + private fun exportToEGFFormat(tournament: Tournament<*>, lines: List, criteria: List, 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, 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") } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TeamHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TeamHandler.kt index 0e9af1b..ad5a5ca 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TeamHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TeamHandler.kt @@ -4,7 +4,6 @@ import com.republicate.kson.Json import com.republicate.kson.toJsonArray import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.model.TeamTournament -import org.jeudego.pairgoth.server.Event import org.jeudego.pairgoth.server.Event.* import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse @@ -26,7 +25,7 @@ object TeamHandler: PairgothApiHandler { val payload = getObjectPayload(request) val team = tournament.teamFromJson(payload) 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) } @@ -38,7 +37,7 @@ object TeamHandler: PairgothApiHandler { val payload = getObjectPayload(request) val updated = tournament.teamFromJson(payload, team) tournament.teams[updated.id] = updated - tournament.dispatchEvent(teamUpdated, team.toJson()) + tournament.dispatchEvent(TeamUpdated, team.toJson()) return Json.Object("success" to true) } @@ -47,7 +46,7 @@ object TeamHandler: PairgothApiHandler { if (tournament !is TeamTournament) badRequest("tournament is not a team tournament") val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid team selector") 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) } } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt index acd3d03..3f8d8c6 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt @@ -11,7 +11,6 @@ import org.jeudego.pairgoth.model.fromJson import org.jeudego.pairgoth.model.toJson import org.jeudego.pairgoth.store.Store import org.jeudego.pairgoth.server.ApiServlet -import org.jeudego.pairgoth.server.Event import org.jeudego.pairgoth.server.Event.* import org.w3c.dom.Element import javax.servlet.http.HttpServletRequest @@ -44,7 +43,7 @@ object TournamentHandler: PairgothApiHandler { else -> badRequest("missing or invalid payload") } Store.addTournament(tournament) - tournament.dispatchEvent(tournamentAdded, tournament.toJson()) + tournament.dispatchEvent(TournamentAdded, tournament.toJson()) return Json.Object("success" to true, "id" to tournament.id) } @@ -64,14 +63,14 @@ object TournamentHandler: PairgothApiHandler { clear() putAll(tournament.games(round)) } - updated.dispatchEvent(tournamentUpdated, updated.toJson()) + updated.dispatchEvent(TournamentUpdated, updated.toJson()) return Json.Object("success" to true) } override fun delete(request: HttpServletRequest): Json { val tournament = getTournament(request) 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) } } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt index b8ae348..35fc196 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt @@ -2,7 +2,7 @@ package org.jeudego.pairgoth.ext import jakarta.xml.bind.JAXBContext import jakarta.xml.bind.JAXBElement -import kotlinx.datetime.LocalDate +import java.time.LocalDate import org.jeudego.pairgoth.model.* import org.jeudego.pairgoth.opengotha.TournamentType import org.jeudego.pairgoth.opengotha.ObjectFactory @@ -13,7 +13,7 @@ import javax.xml.datatype.XMLGregorianCalendar import kotlin.math.roundToInt private const val MILLISECONDS_PER_DAY = 86400000 -fun XMLGregorianCalendar.toLocalDate() = LocalDate(year, month, day) +fun XMLGregorianCalendar.toLocalDate() = LocalDate.of(year, month, day) object OpenGotha { @@ -114,10 +114,10 @@ object OpenGotha { location = genParams.location, online = genParams.isBInternet ?: false, timeSystem = when (genParams.complementaryTimeSystem) { - "SUDDENDEATH" -> SuddenDeath(genParams.basicTime) - "STDBYOYOMI" -> StandardByoyomi(genParams.basicTime, genParams.stdByoYomiTime, 1) // no periods? - "CANBYOYOMI" -> CanadianByoyomi(genParams.basicTime, genParams.canByoYomiTime, genParams.nbMovesCanTime) - "FISCHER" -> FischerTime(genParams.basicTime, genParams.fischerTime) + "SUDDENDEATH" -> SuddenDeath(genParams.basicTime * 60) + "STDBYOYOMI" -> StandardByoyomi(genParams.basicTime * 60, genParams.stdByoYomiTime, 1) // no periods? + "CANBYOYOMI" -> CanadianByoyomi(genParams.basicTime * 60, genParams.canByoYomiTime, genParams.nbMovesCanTime) + "FISCHER" -> FischerTime(genParams.basicTime * 60, genParams.fischerTime) else -> throw Error("missing byoyomi type") }, pairing = when (handParams.hdCeiling) { @@ -145,7 +145,8 @@ object OpenGotha { rating = player.rating, rank = Pairable.parseRank(player.rank), country = player.country, - club = player.club + club = player.club, + final = "FIN" == player.registeringStatus ).also { player.participating.toString().forEachIndexed { i,c -> if (c == '0') it.skip.add(i + 1) @@ -215,7 +216,9 @@ object OpenGotha { player.displayRank() }" rating="${ player.rating - }" ratingOrigin="" registeringStatus="FIN" smmsCorrection="0"/>""" + }" ratingOrigin="" registeringStatus="${ + if (player.final) "FIN" else "PRE" + }" smmsCorrection="0"/>""" } } @@ -269,9 +272,9 @@ object OpenGotha { } - "STDBYOYOMI" + TimeSystem.TimeSystemType.JAPANESE -> "STDBYOYOMI" TimeSystem.TimeSystemType.CANADIAN -> "CANBYOYOMI" TimeSystem.TimeSystemType.FISCHER -> "FISCHER" } }" director="" endDate="${tournament.endDate}" fischerTime="${tournament.timeSystem.increment}" genCountNotPlayedGamesAsHalfPoint="false" genMMBar="${ diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt index b13c502..a182996 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt @@ -7,10 +7,10 @@ import java.util.* // 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 { - val MIN_RANK: Int = -30 // 30k - val MAX_RANK: Int = 8 // 9D + const val MIN_RANK: Int = -30 // 30k + const val MAX_RANK: Int = 8 // 9D } abstract fun toJson(): Json.Object 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 { throw Error("bye player should never be serialized") } @@ -70,8 +70,10 @@ class Player( rating: Int, rank: Int, override var country: String, - override var club: String -): Pairable(id, name, rating, rank) { + override var club: String, + final: Boolean, + mmsCorrection: Int = 0 +): Pairable(id, name, rating, rank, final, mmsCorrection) { companion object // used to store external IDs ("FFG" => FFG ID, "EGF" => EGF PIN, "AGA" => AGA ID ...) val externalIds = mutableMapOf() @@ -82,9 +84,11 @@ class Player( "rating" to rating, "rank" to rank, "country" to country, - "club" to club + "club" to club, + "final" to final ).also { json -> if (skip.isNotEmpty()) json["skip"] = Json.Array(skip) + if (mmsCorrection != 0) json["mmsCorrection"] = mmsCorrection externalIds.forEach { (dbid, 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"), rank = json.getInt("rank") ?: default?.rank ?: badRequest("missing rank"), 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 -> player.skip.clear() json.getArray("skip")?.let { diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt index f287b27..909a4ed 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt @@ -6,6 +6,7 @@ import org.jeudego.pairgoth.model.MainCritParams.SeedMethod.SPLIT_AND_SLIP import org.jeudego.pairgoth.model.PairingType.* import org.jeudego.pairgoth.pairing.solver.MacMahonSolver import org.jeudego.pairgoth.pairing.solver.SwissSolver +import java.util.* // base pairing parameters data class BaseCritParams( @@ -172,7 +173,7 @@ class Swiss( ): Pairing(SWISS, pairingParams, placementParams) { companion object {} override fun pair(tournament: Tournament<*>, round: Int, pairables: List): List { - 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) { companion object {} override fun pair(tournament: Tournament<*>, round: Int, pairables: List): List { - 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() } } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/TimeSystem.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/TimeSystem.kt index 9d3e252..f4e9682 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/TimeSystem.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/TimeSystem.kt @@ -1,7 +1,6 @@ package org.jeudego.pairgoth.model import com.republicate.kson.Json -import org.jeudego.pairgoth.api.ApiHandler import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.model.TimeSystem.TimeSystemType.* @@ -15,7 +14,7 @@ data class TimeSystem( val stones: Int ) { 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) = @@ -30,7 +29,7 @@ fun CanadianByoyomi(mainTime: Int, byoyomi: Int, stones: Int) = fun StandardByoyomi(mainTime: Int, byoyomi: Int, periods: Int) = TimeSystem( - type = STANDARD, + type = JAPANESE, mainTime = mainTime, increment = 0, byoyomi = byoyomi, @@ -86,9 +85,16 @@ fun TimeSystem.Companion.fromJson(json: Json.Object) = 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.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 -> 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) 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 +} diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt index adc3627..a04e5cc 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt @@ -2,7 +2,9 @@ package org.jeudego.pairgoth.model import com.republicate.kson.Json 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.pairing.HistoryHelper 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 kotlin.math.max import kotlin.math.min +import java.util.* import kotlin.math.roundToInt sealed class Tournament ( @@ -76,9 +79,9 @@ sealed class Tournament ( // TODO cleaner solver instantiation val history = historyBefore(round) 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) { - 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") // Recomputes DUDD and hd @@ -88,6 +91,29 @@ sealed class Tournament ( game.drawnUpDown = solver.dudd(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 @@ -133,7 +159,7 @@ class TeamTournament( override val players = mutableMapOf() val teams: MutableMap = _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() val teamPlayers: Set 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() @@ -151,7 +177,8 @@ class TeamTournament( fun teamFromJson(json: Json.Object, default: TeamTournament.Team? = null) = Team( 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 { json.getArray("players")?.let { arr -> arr.mapTo(playerIds) { @@ -174,8 +201,8 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n type = type, name = json.getString("name") ?: default?.name ?: badRequest("missing name"), shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"), - startDate = json.getLocalDate("startDate") ?: default?.startDate ?: badRequest("missing startDate"), - endDate = json.getLocalDate("endDate") ?: default?.endDate ?: badRequest("missing endDate"), + startDate = json.getString("startDate")?.let { LocalDate.parse(it) } ?: default?.startDate ?: badRequest("missing startDate"), + endDate = json.getString("endDate")?.let { LocalDate.parse(it) } ?: default?.endDate ?: badRequest("missing endDate"), country = json.getString("country") ?: default?.country ?: badRequest("missing country"), location = json.getString("location") ?: default?.location ?: badRequest("missing location"), online = json.getBoolean("online") ?: default?.online ?: false, @@ -192,8 +219,8 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n type = type, name = json.getString("name") ?: default?.name ?: badRequest("missing name"), shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"), - startDate = json.getLocalDate("startDate") ?: default?.startDate ?: badRequest("missing startDate"), - endDate = json.getLocalDate("endDate") ?: default?.endDate ?: badRequest("missing endDate"), + startDate = json.getString("startDate")?.let { LocalDate.parse(it) } ?: default?.startDate ?: badRequest("missing startDate"), + endDate = json.getString("endDate")?.let { LocalDate.parse(it) } ?: default?.endDate ?: badRequest("missing endDate"), country = json.getString("country") ?: default?.country ?: badRequest("missing country"), location = json.getString("location") ?: default?.location ?: badRequest("missing location"), online = json.getBoolean("online") ?: default?.online ?: false, diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/BasePairingHelper.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/BasePairingHelper.kt index d743010..dde45a3 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/BasePairingHelper.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/BasePairingHelper.kt @@ -136,11 +136,4 @@ abstract class BasePairingHelper( open fun nameSort(p: Pairable, q: Pairable): Int { 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 - } - } } \ No newline at end of file diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt index 8b32f46..c70bbab 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt @@ -24,6 +24,7 @@ sealed class BaseSolver( pairables: List, // All pairables for this round, it may include the bye player pairing: PairingParams, placement: PlacementParams, + val usedTables: BitSet ) : BasePairingHelper(history, pairables, pairing, placement) { companion object { @@ -540,7 +541,6 @@ sealed class BaseSolver( } open fun games(black: Pairable, white: Pairable): List { // 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) 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))) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt index 45c3324..1a47c12 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt @@ -1,6 +1,7 @@ package org.jeudego.pairgoth.pairing.solver import org.jeudego.pairgoth.model.* +import java.util.* import kotlin.math.max import kotlin.math.min @@ -9,9 +10,9 @@ class MacMahonSolver(round: Int, pairables: List, pairingParams: PairingParams, placementParams: PlacementParams, + usedTables: BitSet, 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 by lazy { 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 // CB TODO - configurable criteria diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/SwissSolver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/SwissSolver.kt index 9a0065d..21b127b 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/SwissSolver.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/SwissSolver.kt @@ -1,13 +1,16 @@ package org.jeudego.pairgoth.pairing.solver import org.jeudego.pairgoth.model.* +import java.util.* class SwissSolver(round: Int, history: List>, pairables: List, pairingParams: PairingParams, - placementParams: PlacementParams): - BaseSolver(round, history, pairables, pairingParams, placementParams) { + placementParams: 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 diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt index 5c38a21..723cae9 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt @@ -218,8 +218,12 @@ class ApiServlet: HttpServlet() { "Missing 'Accept' header" ) // 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 - if (!isJson(accept) && (!isXml(accept) || !request.requestURI.matches(Regex("/api/tour/\\d+")))) throw ApiException( + // 2) there will be other content types: .tou, .h9, .html + 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, "Invalid 'Accept' header" ) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/Event.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/Event.kt index 9557757..57147ee 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/Event.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/Event.kt @@ -4,19 +4,20 @@ import info.macias.sse.events.MessageEvent import java.util.concurrent.atomic.AtomicLong enum class Event { - tournamentAdded, - tournamentUpdated, - tournamentDeleted, - playerAdded, - playerUpdated, - playerDeleted, - teamAdded, - teamUpdated, - teamDeleted, - gamesAdded, - gamesDeleted, - gameUpdated, - resultUpdated, + TournamentAdded, + TournamentUpdated, + TournamentDeleted, + PlayerAdded, + PlayerUpdated, + PlayerDeleted, + TeamAdded, + TeamUpdated, + TeamDeleted, + GamesAdded, + GamesDeleted, + GameUpdated, + ResultUpdated, + TablesRenumbered ; companion object { diff --git a/version.sh b/version.sh new file mode 100755 index 0000000..b2975c6 --- /dev/null +++ b/version.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +mvn versions:set -DnewVersion=$1 diff --git a/view-webapp/pom.xml b/view-webapp/pom.xml index 6e51093..b4add5d 100644 --- a/view-webapp/pom.xml +++ b/view-webapp/pom.xml @@ -153,11 +153,6 @@ kotlin-reflect ${kotlin.version} - - org.jetbrains.kotlinx - kotlinx-datetime-jvm - 0.4.0 - jakarta.servlet diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/FFGRatingsHandler.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/FFGRatingsHandler.kt index eed11c0..977951c 100644 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/FFGRatingsHandler.kt +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/FFGRatingsHandler.kt @@ -25,6 +25,8 @@ object FFGRatingsHandler: RatingsHandler(RatingsManager.Ratings.FFG) { val rating = it["rating"]?.toString()?.toIntOrNull() if (rating != null) { it["rank"] = (rating/100).let { if (it < 0) "${-it}k" else "${it+1}d" } + // then adjust to match EGF ratings + it["rating"] = rating + 2050 } } } diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/PairgothTool.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/PairgothTool.kt index 39c426d..3fece49 100644 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/PairgothTool.kt +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/PairgothTool.kt @@ -9,6 +9,8 @@ import com.republicate.kson.Json class PairgothTool { 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( "NONE" to "No tie break", // No ranking / tie-break @@ -43,4 +45,17 @@ class PairgothTool { "SDC" to "Simplified direct confrontation", // Simplified direct confrontation "DC" to "Direct confrontation", // Direct confrontation ) + + fun getResultsStats(games: Collection): 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) + } } \ No newline at end of file diff --git a/view-webapp/src/main/sass/main.scss b/view-webapp/src/main/sass/main.scss index 8d97404..0477ee6 100644 --- a/view-webapp/src/main/sass/main.scss +++ b/view-webapp/src/main/sass/main.scss @@ -210,6 +210,24 @@ 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 { position: sticky; bottom: 1em; @@ -359,7 +377,7 @@ cursor: pointer; } - thead { + thead th { position: sticky; top: 0; } @@ -371,4 +389,46 @@ a.disabled { 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; + } + } + + } } diff --git a/view-webapp/src/main/sass/tour.scss b/view-webapp/src/main/sass/tour.scss index 96479a0..7d7d6d6 100644 --- a/view-webapp/src/main/sass/tour.scss +++ b/view-webapp/src/main/sass/tour.scss @@ -54,6 +54,18 @@ /* 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 { &.create { .edition { @@ -138,6 +150,35 @@ #player.popup { 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 */ @@ -195,8 +236,30 @@ background-color: rgba(100,200,255,200); 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 { display: flex; flex-flow: column nowrap; @@ -280,4 +343,8 @@ justify-content: space-around; } } + #standings-container { + max-width: 95vw; + overflow-x: auto; + } } diff --git a/view-webapp/src/main/webapp/WEB-INF/translations/fr b/view-webapp/src/main/webapp/WEB-INF/translations/fr index bef15cd..3444910 100644 --- a/view-webapp/src/main/webapp/WEB-INF/translations/fr +++ b/view-webapp/src/main/webapp/WEB-INF/translations/fr @@ -145,4 +145,6 @@ the configuration guide le guide de configuration to à unpairable players joueurs non disponibles version 0.1 supports the version 0.1 supporte le système d’appariement +white blanc white vs. black blanc vs. Noir +confirmed. confirmé(s). \ No newline at end of file diff --git a/view-webapp/src/main/webapp/js/api.js b/view-webapp/src/main/webapp/js/api.js index eaac9b7..0fd329f 100644 --- a/view-webapp/src/main/webapp/js/api.js +++ b/view-webapp/src/main/webapp/js/api.js @@ -9,13 +9,16 @@ const apiVersion = '1.0'; // .catch(err => { ... }); const base = '/api/'; -let headers = function() { +let headers = function(withJson) { let ret = { - "Content-Type": "application/json; charset=utf-8", - "Accept-Version": apiVersion, - "Accept": "application/json", - "X-Browser-Key": store('browserKey') + 'Accept-Version': apiVersion, + 'Accept': 'application/json', + 'X-Browser-Key': store('browserKey') }; + if (typeof(withJson) === 'undefined') withJson = true; + if (withJson) { + ret['Content-Type'] = 'application/json'; + } let accessToken = store('accessToken'); if (accessToken) { ret['Authorization'] = `Bearer ${accessToken}`; diff --git a/view-webapp/src/main/webapp/js/domhelper.js b/view-webapp/src/main/webapp/js/domhelper.js index e2c54d8..d19eb3d 100644 --- a/view-webapp/src/main/webapp/js/domhelper.js +++ b/view-webapp/src/main/webapp/js/domhelper.js @@ -120,8 +120,14 @@ Element.prototype.hide = function() { return this; } NodeList.prototype.text = function(txt) { - this.item(0).text(txt); - return this; + if (typeof(txt) === 'undefined') { + return this.item(0).text(); + } else { + this.forEach(elem => { + elem.text(txt); + }); + return this; + } } Element.prototype.text = function(txt) { if (typeof(txt) === 'undefined') { diff --git a/view-webapp/src/main/webapp/js/main.js b/view-webapp/src/main/webapp/js/main.js index 5cbeb44..d0881b7 100644 --- a/view-webapp/src/main/webapp/js/main.js +++ b/view-webapp/src/main/webapp/js/main.js @@ -161,6 +161,7 @@ function modal(id) { function close_modal() { $('body').removeClass('dimmed'); $(`.popup`).removeClass('shown'); + store('addingPlayers', false); } function downloadFile(blob, filename) { @@ -189,44 +190,55 @@ onLoad(() => { // keyboard handling document.on('keyup', e => { + let tab = document.location.hash; switch (e.key) { case 'Escape': { - if ($('#player').hasClass('shown') && $('#needle')[0].value) { + if (tab === '#registration') { + if ($('#player').hasClass('shown') && $('#needle')[0].value) { $('#needle')[0].value = ''; initSearch(); - } else { - close_modal(); + } else { + close_modal(); + } + } else if (tab === '#pairing') { + $('#pairing-lists .selected.listitem').removeClass('selected'); } break; } case 'ArrowDown': { - if (typeof(searchResultShown) === 'function' && searchResultShown()) { - let lines = $('.result-line'); - if (typeof (searchHighlight) === 'undefined') searchHighlight = 0; - else ++searchHighlight; - searchHighlight = Math.min(searchHighlight, lines.length - 1); - lines.removeClass('highlighted'); - lines[searchHighlight].addClass('highlighted'); + if (tab === '#registration') { + if (typeof(searchResultShown) === 'function' && searchResultShown()) { + let lines = $('.result-line'); + if (typeof (searchHighlight) === 'undefined') searchHighlight = 0; + else ++searchHighlight; + searchHighlight = Math.min(searchHighlight, lines.length - 1); + lines.removeClass('highlighted'); + lines[searchHighlight].addClass('highlighted'); + } } break; } case 'ArrowUp': { - if (typeof(searchResultShown) === 'function' && searchResultShown()) { - let lines = $('.result-line'); - if (typeof (searchHighlight) === 'undefined') searchHighlight = 0; - else --searchHighlight; - searchHighlight = Math.max(searchHighlight, 0); - lines.removeClass('highlighted'); - lines[searchHighlight].addClass('highlighted'); + if (tab === '#registration') { + if (typeof(searchResultShown) === 'function' && searchResultShown()) { + let lines = $('.result-line'); + if (typeof (searchHighlight) === 'undefined') searchHighlight = 0; + else --searchHighlight; + searchHighlight = Math.max(searchHighlight, 0); + lines.removeClass('highlighted'); + lines[searchHighlight].addClass('highlighted'); + } } break; } case 'Enter': { - if (typeof(searchResultShown) === 'function') { - if (searchResultShown()) { - fillPlayer(searchResult[searchHighlight]); - } else { - $('#register')[0].click(); + if (tab === '#registration') { + if (typeof(searchResultShown) === 'function') { + if (searchResultShown()) { + fillPlayer(searchResult[searchHighlight]); + } else { + $('#register')[0].click(); + } } } break; @@ -241,5 +253,18 @@ onLoad(() => { }, 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); + } + }); diff --git a/view-webapp/src/main/webapp/js/tour-information.inc.js b/view-webapp/src/main/webapp/js/tour-information.inc.js index 09a6995..5723567 100644 --- a/view-webapp/src/main/webapp/js/tour-information.inc.js +++ b/view-webapp/src/main/webapp/js/tour-information.inc.js @@ -118,7 +118,6 @@ onLoad(() => { $('#tournament-infos').on('submit', e => { e.preventDefault(); let form = e.target; - console.log(form.val('country')); let tour = { name: form.val('name'), shortName: form.val('shortName'), diff --git a/view-webapp/src/main/webapp/js/tour-pairing.inc.js b/view-webapp/src/main/webapp/js/tour-pairing.inc.js index 55a3011..6e0ebba 100644 --- a/view-webapp/src/main/webapp/js/tour-pairing.inc.js +++ b/view-webapp/src/main/webapp/js/tour-pairing.inc.js @@ -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(()=>{ $('.listitem').on('click', e => { if (e.shiftKey && typeof(focused) !== 'undefined') { @@ -31,13 +51,17 @@ onLoad(()=>{ let parent = e.target.closest('.multi-select'); let children = parent.childNodes.filter('.listitem'); for (let j = from; j <= to; ++j) { new Tablesort($('#players')[0]); - children.item(j).addClass('selected'); children.item(j).attr('draggable', true); } } else { let target = e.target.closest('.listitem'); - focused = target.toggleClass('selected').attr('draggable', target.hasClass('selected')); + if (e.detail === 1) { + focused = target.toggleClass('selected').attr('draggable', target.hasClass('selected')); + } else { + focused = target.attr('draggable', target.hasClass('selected')); + editGame(focused); + } } }); $('#pair').on('click', e => { @@ -56,4 +80,39 @@ onLoad(()=>{ } 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(); + } + }); + }); }); diff --git a/view-webapp/src/main/webapp/js/tour-registration.inc.js b/view-webapp/src/main/webapp/js/tour-registration.inc.js index eb494ef..64394fd 100644 --- a/view-webapp/src/main/webapp/js/tour-registration.inc.js +++ b/view-webapp/src/main/webapp/js/tour-registration.inc.js @@ -67,18 +67,45 @@ function parseRank(rank) { } function fillPlayer(player) { + // hack UK / GB + let country = player.country.toLowerCase(); + if ('uk' === country) country = 'gb'; let form = $('#player-form')[0]; form.val('name', player.name); 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('rank', parseRank(player.rank)); form.val('rating', player.rating); + form.val('final', false); $('#needle')[0].value = ''; initSearch(); $('#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(() => { $('input.numeric').imask({ mask: Number, @@ -86,15 +113,39 @@ onLoad(() => { min: 0, 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 => { - let form = $('#player-form')[0]; - form.addClass('add'); - // $('#player-form input.participation').forEach(chk => chk.checked = true); - form.reset(); - $('#player').removeClass('edit').addClass('create'); - modal('player'); - $('#needle').focus(); + addPlayers(); }); $('#cancel-register').on('click', e => { e.preventDefault(); @@ -122,7 +173,6 @@ onLoad(() => { $('#player-form')[0].dispatchEvent(new CustomEvent('submit', {cancelable: true})); }); $('#player-form').on('submit', e => { - ("submitting!!") e.preventDefault(); let form = $('#player-form')[0]; let player = { @@ -132,11 +182,13 @@ onLoad(() => { rank: form.val('rank'), country: form.val('country'), 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')) { api.postJson(`tour/${tour_id}/part`, player) .then(player => { + console.log(player) if (player !== 'error') { window.location.reload(); } @@ -146,6 +198,7 @@ onLoad(() => { player['id'] = id; api.putJson(`tour/${tour_id}/part/${id}`, player) .then(player => { + console.log(player) if (player !== 'error') { window.location.reload(); } @@ -153,6 +206,8 @@ onLoad(() => { } }); $('#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'); api.getJson(`tour/${tour_id}/part/${id}`) .then(player => { @@ -163,8 +218,11 @@ onLoad(() => { form.val('firstname', player.firstname); form.val('rating', player.rating); form.val('rank', player.rank); - form.val('country', player.country); + form.val('country', player.country.toLowerCase()); 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) { form.val(`r${r}`, !(player.skip && player.skip.includes(r))); } @@ -192,6 +250,9 @@ onLoad(() => { let chk = e.target.closest('.toggle'); let checkbox = chk.find('input')[0]; checkbox.checked = !checkbox.checked; + let id = checkbox.getAttribute('id'); + let value = checkbox.checked; + store(id, value); initSearch(); }); 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(); + } }); diff --git a/view-webapp/src/main/webapp/js/tour-results.inc.js b/view-webapp/src/main/webapp/js/tour-results.inc.js index 7889f1e..648a2e7 100644 --- a/view-webapp/src/main/webapp/js/tour-results.inc.js +++ b/view-webapp/src/main/webapp/js/tour-results.inc.js @@ -1,4 +1,4 @@ -function setResult(id, result) { +function setResult(id, result, previous) { api.putJson(`tour/${tour_id}/res/${activeRound}`, { id: id, result: result }) .then(res => { if (res !== 'error') { @@ -9,15 +9,26 @@ function setResult(id, result) { let dispResult = result; switch (result) { case '?': break; - case 'w': white.addClass('winner'); black.addClass('looser'); dispResult = 'w+'; break; - case 'b': black.addClass('winner'); white.addClass('looser'); dispResult = 'b+'; break; - case '=': break; + case 'w': white.addClass('winner'); black.addClass('looser'); dispResult = '1-0'; break; + case 'b': black.addClass('winner'); white.addClass('looser'); dispResult = '0-1'; break; + case '=': dispResult = '½-½'; break; case 'X': break; case '#': white.addClass('winner'); black.addClass('winner'); dispResult = '1-1'; break; case '0': white.addClass('looser'); black.addClass('looser'); dispResult = '0-0'; break; } let resultCell = row.find('td.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 => { let cell = e.target.closest('.result'); let gameId = e.target.closest('tr').data('id'); - let result = cell.data('result'); - let index = results.indexOf(result); - console.log(index) - result = results[(index + 1)%results.length]; - console.log(result) - setResult(gameId, result); + let oldResult = cell.data('result'); + let index = results.indexOf(oldResult); + let newResult = results[(index + 1)%results.length]; + setResult(gameId, newResult, oldResult); }); }); diff --git a/view-webapp/src/main/webapp/js/tour-standings.inc.js b/view-webapp/src/main/webapp/js/tour-standings.inc.js index d9f74e9..9c0b6d6 100644 --- a/view-webapp/src/main/webapp/js/tour-standings.inc.js +++ b/view-webapp/src/main/webapp/js/tour-standings.inc.js @@ -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(() => { $('.criterium').on('click', e => { let alreadyOpen = e.target.closest('select'); @@ -45,4 +62,10 @@ onLoad(() => { $('#publish-modal').on('click', e => { close_modal(); }); + $('.publish-ffg').on('click', e => { + publish('ffg', 'tou'); + }); + $('.publish-egf').on('click', e => { + publish('egf', 'h9'); + }); }); diff --git a/view-webapp/src/main/webapp/lib/tablesort-5.4.0/tablesort.min.js b/view-webapp/src/main/webapp/lib/tablesort-5.4.0/tablesort.min.js index 6fb74cb..f7cc8d0 100644 --- a/view-webapp/src/main/webapp/lib/tablesort-5.4.0/tablesort.min.js +++ b/view-webapp/src/main/webapp/lib/tablesort-5.4.0/tablesort.min.js @@ -1,6 +1 @@ -/*! - * 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:t0)if(e.tHead&&e.tHead.rows.length>0){for(s=0;s0&&p.push(c),d++;if(!p)return}for(d=0;d
- #foreach($game in $games) - #set($white = $pmap[$game.w]) - #set($black = $pmap[$game.b]) -
#$game.t#if($white)$white.name $white.firstname #rank($white.rank)#{else}BIP#end #if($black)$black.name $black.firstname #rank($black.rank)#{else}BIP#end#if($game.h)h$game.h#end
- #end +#foreach($game in $games) + #set($white = $pmap[$game.w]) + #set($black = $pmap[$game.b]) +
+
#$game.t
+
#if($white)$white.name $white.firstname#{else}BIP#end
+
#if($white)#rank($white.rank)#end / #if($black)#rank($black.rank)#end
+
#if($black)$black.name $black.firstname#{else}BIP#end
+
#if($game.h)h$game.h#{else} #end
+
+#end
+ + diff --git a/view-webapp/src/main/webapp/tour-registration.inc.html b/view-webapp/src/main/webapp/tour-registration.inc.html index 379f60c..067d86f 100644 --- a/view-webapp/src/main/webapp/tour-registration.inc.html +++ b/view-webapp/src/main/webapp/tour-registration.inc.html @@ -1,24 +1,40 @@ -
-
-
#set($parts = $api.get("tour/${params.id}/part")) #set($pmap = $utils.toMap($parts)) +
+
+
+
+ + +
+
+ $parts.size() participants, $utils.countFinals($parts) confirmed. +
+
+ +
+ - - + ## TableSort bug which inverts specified sort... + + #foreach($part in $parts) - + + - + @@ -141,6 +157,14 @@
+ +
+ + +
#foreach($r in [1..$tour.rounds])
@@ -154,7 +178,7 @@ $round +#set($stats = $utils.getResultsStats($games)) + $stats.known / $stats.total
Reg Name First name Country Club RankRatingParticipationRatingParticipation
+ + + $part.name $part.firstname$part.country$part.country.toUpperCase() $part.club #rank($part.rank) $part.rating
@@ -14,7 +16,7 @@ -#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) #set($white = $pmap[$game.w]) #set($black = $pmap[$game.b]) @@ -23,7 +25,7 @@ - + #end #end diff --git a/view-webapp/src/main/webapp/tour-standings.inc.html b/view-webapp/src/main/webapp/tour-standings.inc.html index 5824408..0871ad5 100644 --- a/view-webapp/src/main/webapp/tour-standings.inc.html +++ b/view-webapp/src/main/webapp/tour-standings.inc.html @@ -66,7 +66,7 @@ - + #set($mx = $round - 1) @@ -101,9 +101,9 @@ diff --git a/view-webapp/src/main/webapp/tour.html b/view-webapp/src/main/webapp/tour.html index 3a9b291..bb22aec 100644 --- a/view-webapp/src/main/webapp/tour.html +++ b/view-webapp/src/main/webapp/tour.html @@ -73,6 +73,8 @@ const tour_id = ${tour.id}; const tour_rounds = ${tour.rounds}; let activeRound = ${round}; + let standingsUpToDate = true; + let pairablesUpToDate = true; // $params #end #set($datepickerLocale = $translate.datepickerLocale($request.lang, $request.loc)) @@ -105,6 +107,11 @@ $('.step').removeClass('active'); $(`.step[data-step="${step}"], #${step}-tab`).addClass('active'); window.location.hash = `#${step}`; + if (step === 'standings' && !standingsUpToDate) { + window.location.reload(); + } else if (step === 'pairing' && !pairablesUpToDate) { + window.location.reload(); + } } onLoad(() => {
result
#$game.t #if($white)$white.name $white.firstname #rank($white.rank)#{else}BIP#end #if($black)$black.name $black.firstname #rank($black.rank)#{else}BIP#end$dispRst[$game.r]$dispRst[$game.r]
$part.num $part.place $part.name $part.firstname#rank($part.rank)#rank($part.rank) $part.country $number.format('0.#', $part.NBW)