From b261e568072b38b2161d889a32db21fc5a2f4d6e Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Mon, 1 Jan 2024 12:26:40 +0100 Subject: [PATCH 01/18] EGF and FFG exports --- .../jeudego/pairgoth/api/StandingsHandler.kt | 168 +++++++++++++++--- .../org/jeudego/pairgoth/ext/OpenGotha.kt | 16 +- .../org/jeudego/pairgoth/model/TimeSystem.kt | 14 +- .../org/jeudego/pairgoth/model/Tournament.kt | 10 +- .../org/jeudego/pairgoth/server/ApiServlet.kt | 8 +- view-webapp/pom.xml | 5 - view-webapp/src/main/sass/tour.scss | 9 + view-webapp/src/main/webapp/js/api.js | 13 +- .../src/main/webapp/js/tour-standings.inc.js | 23 +++ .../src/main/webapp/tour-standings.inc.html | 6 +- 10 files changed, 218 insertions(+), 54 deletions(-) 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..d4a7480 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? { @@ -83,7 +89,7 @@ object StandingsHandler: PairgothApiHandler { 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/ext/OpenGotha.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt index b8ae348..cd74f6e 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) { @@ -269,9 +269,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/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 b380dd6..d610218 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,7 @@ package org.jeudego.pairgoth.model import com.republicate.kson.Json import com.republicate.kson.toJsonArray -import kotlinx.datetime.LocalDate +import java.time.LocalDate import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.pairing.solver.MacMahonSolver import org.jeudego.pairgoth.pairing.solver.SwissSolver @@ -169,8 +169,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, @@ -187,8 +187,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/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/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/sass/tour.scss b/view-webapp/src/main/sass/tour.scss index 96479a0..1e0230f 100644 --- a/view-webapp/src/main/sass/tour.scss +++ b/view-webapp/src/main/sass/tour.scss @@ -54,6 +54,11 @@ /* registration section */ + #players-list { + max-width: 95vw; + overflow-x: auto; + } + #player { &.create { .edition { @@ -280,4 +285,8 @@ justify-content: space-around; } } + #standings-container { + max-width: 95vw; + overflow-x: auto; + } } 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/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/tour-standings.inc.html b/view-webapp/src/main/webapp/tour-standings.inc.html index 5824408..9194f5c 100644 --- a/view-webapp/src/main/webapp/tour-standings.inc.html +++ b/view-webapp/src/main/webapp/tour-standings.inc.html @@ -101,9 +101,9 @@ From 897246c7a6f1699609feebbbcfa0fa9ea882b11f Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Thu, 18 Jan 2024 06:57:34 +0100 Subject: [PATCH 02/18] Review paired list content layout ; ESC for unselection --- view-webapp/src/main/sass/tour.scss | 16 ++++++ view-webapp/src/main/webapp/js/main.js | 56 +++++++++++-------- .../src/main/webapp/tour-pairing.inc.html | 16 ++++-- 3 files changed, 61 insertions(+), 27 deletions(-) diff --git a/view-webapp/src/main/sass/tour.scss b/view-webapp/src/main/sass/tour.scss index 1e0230f..78386a0 100644 --- a/view-webapp/src/main/sass/tour.scss +++ b/view-webapp/src/main/sass/tour.scss @@ -202,6 +202,22 @@ } } } + #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; diff --git a/view-webapp/src/main/webapp/js/main.js b/view-webapp/src/main/webapp/js/main.js index 5cbeb44..6ff4174 100644 --- a/view-webapp/src/main/webapp/js/main.js +++ b/view-webapp/src/main/webapp/js/main.js @@ -189,44 +189,56 @@ 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') { + console.log('here') + $('#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; diff --git a/view-webapp/src/main/webapp/tour-pairing.inc.html b/view-webapp/src/main/webapp/tour-pairing.inc.html index 9468c74..05f2a95 100644 --- a/view-webapp/src/main/webapp/tour-pairing.inc.html +++ b/view-webapp/src/main/webapp/tour-pairing.inc.html @@ -48,11 +48,17 @@
- #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
From cffa4ce699fbc53a9dd7f4cd5d4df49dfc611683 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Fri, 19 Jan 2024 05:43:07 +0100 Subject: [PATCH 03/18] Game edition ok, and other minor tweaks --- .../org/jeudego/pairgoth/api/ApiHandler.kt | 2 +- .../pairgoth/api/PairgothApiHandler.kt | 2 +- .../jeudego/pairgoth/api/PairingHandler.kt | 1 + view-webapp/src/main/sass/tour.scss | 3 + view-webapp/src/main/webapp/js/domhelper.js | 10 ++- .../src/main/webapp/js/tour-pairing.inc.js | 63 ++++++++++++++++++- .../main/webapp/js/tour-registration.inc.js | 1 - .../src/main/webapp/js/tour-results.inc.js | 6 +- .../src/main/webapp/tour-pairing.inc.html | 57 +++++++++++++++-- 9 files changed, 131 insertions(+), 14 deletions(-) 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..58a813a 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 ef15003..ac005ce 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 @@ -71,6 +71,7 @@ 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) 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.recomputeDUDD(round, game.id) diff --git a/view-webapp/src/main/sass/tour.scss b/view-webapp/src/main/sass/tour.scss index 78386a0..0c565f1 100644 --- a/view-webapp/src/main/sass/tour.scss +++ b/view-webapp/src/main/sass/tour.scss @@ -202,6 +202,9 @@ } } } + #pairables { + margin-bottom: 1em; + } #paired { .listitem { position: relative; 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/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..e2f6a4c 100644 --- a/view-webapp/src/main/webapp/js/tour-registration.inc.js +++ b/view-webapp/src/main/webapp/js/tour-registration.inc.js @@ -122,7 +122,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 = { 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..9320a53 100644 --- a/view-webapp/src/main/webapp/js/tour-results.inc.js +++ b/view-webapp/src/main/webapp/js/tour-results.inc.js @@ -9,9 +9,9 @@ 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; diff --git a/view-webapp/src/main/webapp/tour-pairing.inc.html b/view-webapp/src/main/webapp/tour-pairing.inc.html index 05f2a95..52d1ca3 100644 --- a/view-webapp/src/main/webapp/tour-pairing.inc.html +++ b/view-webapp/src/main/webapp/tour-pairing.inc.html @@ -52,11 +52,11 @@ #set($white = $pmap[$game.w]) #set($black = $pmap[$game.b])
-
#$game.t
-
#if($white)$white.name $white.firstname#{else}BIP#end
+
#$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
+
#if($black)$black.name $black.firstname#{else}BIP#end
+
#if($game.h)h$game.h#{else} #end
#end @@ -64,3 +64,52 @@ + + From 3c9e1683f7b06ed2d79def00aac0a2ef3fe95a5a Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 20 Jan 2024 09:09:39 +0100 Subject: [PATCH 04/18] Registration status in progress --- .../org/jeudego/pairgoth/ext/OpenGotha.kt | 7 ++- .../org/jeudego/pairgoth/model/Pairable.kt | 19 +++--- .../org/jeudego/pairgoth/model/Tournament.kt | 5 +- view-webapp/src/main/sass/tour.scss | 29 ++++++++++ .../main/webapp/js/tour-registration.inc.js | 58 ++++++++++++++++++- .../src/main/webapp/js/tour-results.inc.js | 1 + .../main/webapp/tour-registration.inc.html | 24 +++++++- .../src/main/webapp/tour-results.inc.html | 2 +- view-webapp/src/main/webapp/tour.html | 4 ++ 9 files changed, 131 insertions(+), 18 deletions(-) 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 cd74f6e..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 @@ -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"/>""" } } 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..068c681 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) { 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,9 @@ 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 +): Pairable(id, name, rating, rank, final) { companion object // used to store external IDs ("FFG" => FFG ID, "EGF" => EGF PIN, "AGA" => AGA ID ...) val externalIds = mutableMapOf() @@ -82,7 +83,8 @@ 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) externalIds.forEach { (dbid, id) -> @@ -103,7 +105,8 @@ 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 ).also { player -> player.skip.clear() json.getArray("skip")?.let { 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 d610218..fc2e116 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 @@ -128,7 +128,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() @@ -146,7 +146,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) { diff --git a/view-webapp/src/main/sass/tour.scss b/view-webapp/src/main/sass/tour.scss index 0c565f1..590ba9c 100644 --- a/view-webapp/src/main/sass/tour.scss +++ b/view-webapp/src/main/sass/tour.scss @@ -143,6 +143,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 */ 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 e2f6a4c..ed562e1 100644 --- a/view-webapp/src/main/webapp/js/tour-registration.inc.js +++ b/view-webapp/src/main/webapp/js/tour-registration.inc.js @@ -67,13 +67,17 @@ 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()); + 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(); @@ -90,8 +94,10 @@ onLoad(() => { $('#add').on('click', e => { let form = $('#player-form')[0]; form.addClass('add'); - // $('#player-form input.participation').forEach(chk => chk.checked = true); + // keep preliminary/final status + let status = form.val('final') || false; form.reset(); + form.val('final', status); $('#player').removeClass('edit').addClass('create'); modal('player'); $('#needle').focus(); @@ -131,7 +137,8 @@ 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) @@ -152,6 +159,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 => { @@ -164,6 +173,9 @@ onLoad(() => { form.val('rank', player.rank); form.val('country', player.country); 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))); } @@ -210,4 +222,44 @@ 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'); + } + }); + 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'); + }); }); 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 9320a53..5611892 100644 --- a/view-webapp/src/main/webapp/js/tour-results.inc.js +++ b/view-webapp/src/main/webapp/js/tour-results.inc.js @@ -18,6 +18,7 @@ function setResult(id, result) { } let resultCell = row.find('td.result'); resultCell.text(dispResult).data('result', result); + standingsUpToDate = false; } }) } diff --git a/view-webapp/src/main/webapp/tour-registration.inc.html b/view-webapp/src/main/webapp/tour-registration.inc.html index 379f60c..e69a419 100644 --- a/view-webapp/src/main/webapp/tour-registration.inc.html +++ b/view-webapp/src/main/webapp/tour-registration.inc.html @@ -1,10 +1,18 @@
+
+
+ + +
+
+
#set($parts = $api.get("tour/${params.id}/part")) #set($pmap = $utils.toMap($parts)) + @@ -15,10 +23,14 @@ #foreach($part in $parts) - + + - + @@ -141,6 +153,14 @@
+ +
+ + +
#foreach($r in [1..$tour.rounds])
diff --git a/view-webapp/src/main/webapp/tour-results.inc.html b/view-webapp/src/main/webapp/tour-results.inc.html index 1af39dd..bbbf404 100644 --- a/view-webapp/src/main/webapp/tour-results.inc.html +++ b/view-webapp/src/main/webapp/tour-results.inc.html @@ -14,7 +14,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]) diff --git a/view-webapp/src/main/webapp/tour.html b/view-webapp/src/main/webapp/tour.html index 3a9b291..9857676 100644 --- a/view-webapp/src/main/webapp/tour.html +++ b/view-webapp/src/main/webapp/tour.html @@ -73,6 +73,7 @@ const tour_id = ${tour.id}; const tour_rounds = ${tour.rounds}; let activeRound = ${round}; + let standingsUpToDate = true; // $params #end #set($datepickerLocale = $translate.datepickerLocale($request.lang, $request.loc)) @@ -105,6 +106,9 @@ $('.step').removeClass('active'); $(`.step[data-step="${step}"], #${step}-tab`).addClass('active'); window.location.hash = `#${step}`; + if (step === 'standings' && !standingsUpToDate) { + window.location.reload(); + } } onLoad(() => { From d0ff355c11832b69f1ad26b310960c4c16115346 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 20 Jan 2024 10:27:00 +0100 Subject: [PATCH 05/18] Persistent table sort on registration page --- view-webapp/src/main/sass/main.scss | 2 +- view-webapp/src/main/webapp/js/main.js | 1 - .../main/webapp/js/tour-information.inc.js | 1 - .../main/webapp/js/tour-registration.inc.js | 41 ++++++++++++++++++- .../src/main/webapp/js/tour-results.inc.js | 2 - .../lib/tablesort-5.4.0/tablesort.min.js | 7 +--- .../main/webapp/tour-registration.inc.html | 5 ++- .../src/main/webapp/tour-standings.inc.html | 2 +- 8 files changed, 45 insertions(+), 16 deletions(-) diff --git a/view-webapp/src/main/sass/main.scss b/view-webapp/src/main/sass/main.scss index 8d97404..09651bf 100644 --- a/view-webapp/src/main/sass/main.scss +++ b/view-webapp/src/main/sass/main.scss @@ -359,7 +359,7 @@ cursor: pointer; } - thead { + thead th { position: sticky; top: 0; } diff --git a/view-webapp/src/main/webapp/js/main.js b/view-webapp/src/main/webapp/js/main.js index 6ff4174..f416440 100644 --- a/view-webapp/src/main/webapp/js/main.js +++ b/view-webapp/src/main/webapp/js/main.js @@ -200,7 +200,6 @@ onLoad(() => { close_modal(); } } else if (tab === '#pairing') { - console.log('here') $('#pairing-lists .selected.listitem').removeClass('selected'); } break; 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-registration.inc.js b/view-webapp/src/main/webapp/js/tour-registration.inc.js index ed562e1..27b04c1 100644 --- a/view-webapp/src/main/webapp/js/tour-registration.inc.js +++ b/view-webapp/src/main/webapp/js/tour-registration.inc.js @@ -73,6 +73,7 @@ function fillPlayer(player) { let form = $('#player-form')[0]; form.val('name', player.name); form.val('firstname', player.firstname); + console.log(country); form.val('country', country); form.val('club', player.club); form.val('rank', parseRank(player.rank)); @@ -83,6 +84,8 @@ function fillPlayer(player) { $('#register').focus(); } +let tableSort; + onLoad(() => { $('input.numeric').imask({ mask: Number, @@ -90,7 +93,37 @@ 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'); @@ -110,6 +143,7 @@ onLoad(() => { }); $('#register').on('click', e => { + console.log("clicked") let form = e.target.closest('form'); let valid = true; let required = ['name', 'firstname', 'country', 'club', 'rank', 'rating']; @@ -128,6 +162,7 @@ onLoad(() => { $('#player-form')[0].dispatchEvent(new CustomEvent('submit', {cancelable: true})); }); $('#player-form').on('submit', e => { + console.log("submit") e.preventDefault(); let form = $('#player-form')[0]; let player = { @@ -143,6 +178,7 @@ onLoad(() => { if (form.hasClass('add')) { api.postJson(`tour/${tour_id}/part`, player) .then(player => { + console.log(player) if (player !== 'error') { window.location.reload(); } @@ -152,6 +188,7 @@ onLoad(() => { player['id'] = id; api.putJson(`tour/${tour_id}/part/${id}`, player) .then(player => { + console.log(player) if (player !== 'error') { window.location.reload(); } @@ -171,7 +208,7 @@ 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'); 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 5611892..9a76861 100644 --- a/view-webapp/src/main/webapp/js/tour-results.inc.js +++ b/view-webapp/src/main/webapp/js/tour-results.inc.js @@ -38,9 +38,7 @@ onLoad(()=>{ 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); }); }); 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;dCountry - - + ## TableSort bug which inverts specified sort... + + #foreach($part in $parts) diff --git a/view-webapp/src/main/webapp/tour-standings.inc.html b/view-webapp/src/main/webapp/tour-standings.inc.html index 9194f5c..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) From c277b5981d23c4f9218fb9672b12fa108a79fdca Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 20 Jan 2024 10:56:10 +0100 Subject: [PATCH 06/18] Persistent scroll --- view-webapp/src/main/webapp/js/main.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/view-webapp/src/main/webapp/js/main.js b/view-webapp/src/main/webapp/js/main.js index f416440..f0ffa38 100644 --- a/view-webapp/src/main/webapp/js/main.js +++ b/view-webapp/src/main/webapp/js/main.js @@ -252,5 +252,22 @@ onLoad(() => { }, 1); } + // persistent scroll + $('#center').on('scroll', e => { + let scroll = $('#center')[0].scrollTop; + console.log(`scroll=${scroll}`); + store('scroll', scroll); + }); + let persistentScroll = store('scroll'); + if (persistentScroll) { + setTimeout(() => { + console.log(`scrollHeight = ${$('#center')[0].scrollHeight}`); + console.log(`scrolling to ${persistentScroll}`); + $('#center')[0].scrollTop = persistentScroll; + let scroll = $('#center')[0].scrollTop; + console.log(`scrolled to ${scroll}`); + }, 200); + } + }); From 5890ba6f413e9734d6a8c1fa78533cb0e9acd384 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 20 Jan 2024 11:17:38 +0100 Subject: [PATCH 07/18] Persistence of search toggles --- view-webapp/src/main/webapp/js/main.js | 4 ---- .../src/main/webapp/js/tour-registration.inc.js | 12 ++++++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/view-webapp/src/main/webapp/js/main.js b/view-webapp/src/main/webapp/js/main.js index f0ffa38..8a17e8c 100644 --- a/view-webapp/src/main/webapp/js/main.js +++ b/view-webapp/src/main/webapp/js/main.js @@ -255,17 +255,13 @@ onLoad(() => { // persistent scroll $('#center').on('scroll', e => { let scroll = $('#center')[0].scrollTop; - console.log(`scroll=${scroll}`); store('scroll', scroll); }); let persistentScroll = store('scroll'); if (persistentScroll) { setTimeout(() => { - console.log(`scrollHeight = ${$('#center')[0].scrollHeight}`); - console.log(`scrolling to ${persistentScroll}`); $('#center')[0].scrollTop = persistentScroll; let scroll = $('#center')[0].scrollTop; - console.log(`scrolled to ${scroll}`); }, 200); } 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 27b04c1..596273a 100644 --- a/view-webapp/src/main/webapp/js/tour-registration.inc.js +++ b/view-webapp/src/main/webapp/js/tour-registration.inc.js @@ -130,6 +130,13 @@ onLoad(() => { // 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'); @@ -143,7 +150,6 @@ onLoad(() => { }); $('#register').on('click', e => { - console.log("clicked") let form = e.target.closest('form'); let valid = true; let required = ['name', 'firstname', 'country', 'club', 'rank', 'rating']; @@ -162,7 +168,6 @@ onLoad(() => { $('#player-form')[0].dispatchEvent(new CustomEvent('submit', {cancelable: true})); }); $('#player-form').on('submit', e => { - console.log("submit") e.preventDefault(); let form = $('#player-form')[0]; let player = { @@ -240,6 +245,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 => { From 0fba3e065cba2ac32ccadcd31b1d18527b459257 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 20 Jan 2024 11:35:59 +0100 Subject: [PATCH 08/18] Final reg status handling tweaks --- .../kotlin/org/jeudego/pairgoth/api/PairingHandler.kt | 9 ++++++--- view-webapp/src/main/webapp/js/tour-registration.inc.js | 2 ++ view-webapp/src/main/webapp/tour.html | 3 +++ 3 files changed, 11 insertions(+), 3 deletions(-) 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 ac005ce..a9ba349 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 @@ -21,8 +21,8 @@ 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 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 return Json.Object( "games" to games.map { it.toJson() }.toCollection(Json.MutableArray()), @@ -44,12 +44,13 @@ 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") @@ -79,6 +80,8 @@ object PairingHandler: PairgothApiHandler { 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") 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 596273a..f8eaa06 100644 --- a/view-webapp/src/main/webapp/js/tour-registration.inc.js +++ b/view-webapp/src/main/webapp/js/tour-registration.inc.js @@ -288,6 +288,8 @@ onLoad(() => { }).then(player => { if (player !== 'error') { cell.toggleClass('final'); + standingsUpToDate = false; + pairablesUpToDate = false; } }); e.preventDefault(); diff --git a/view-webapp/src/main/webapp/tour.html b/view-webapp/src/main/webapp/tour.html index 9857676..bb22aec 100644 --- a/view-webapp/src/main/webapp/tour.html +++ b/view-webapp/src/main/webapp/tour.html @@ -74,6 +74,7 @@ const tour_rounds = ${tour.rounds}; let activeRound = ${round}; let standingsUpToDate = true; + let pairablesUpToDate = true; // $params #end #set($datepickerLocale = $translate.datepickerLocale($request.lang, $request.loc)) @@ -108,6 +109,8 @@ window.location.hash = `#${step}`; if (step === 'standings' && !standingsUpToDate) { window.location.reload(); + } else if (step === 'pairing' && !pairablesUpToDate) { + window.location.reload(); } } From 04f9c27cbe3aa7e2651a83a84ad565f878127032 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 20 Jan 2024 11:41:34 +0100 Subject: [PATCH 09/18] Don't list non final pairables in standings --- .../main/kotlin/org/jeudego/pairgoth/api/StandingsHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d4a7480..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 @@ -84,7 +84,7 @@ 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 From 48fc66ab527813bd6679154dbce73dcb216bd20a Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 20 Jan 2024 12:16:09 +0100 Subject: [PATCH 10/18] Very basic printing css --- view-webapp/src/main/sass/main.scss | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/view-webapp/src/main/sass/main.scss b/view-webapp/src/main/sass/main.scss index 09651bf..7859971 100644 --- a/view-webapp/src/main/sass/main.scss +++ b/view-webapp/src/main/sass/main.scss @@ -371,4 +371,35 @@ a.disabled { color: darkgray; } + + @media print { + + body { + width: unset; + height: unset; + font-size: 0.65em; + } + + .roundbox { + border: none; + } + + #title { + font-size: 1rem; + margin-top: 0; + } + + #logo, #lang, .steps, #filter-box, #footer, #pairing-left, #pairing-buttons, button, #standings-params { + display: none !important; + } + + .circular.label { + transform: scale(0.7); + } + + #paired { + max-height: unset; + } + + } } From fd1df8762aee302865df7ef3d367da213e00f4db Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 20 Jan 2024 12:38:35 +0100 Subject: [PATCH 11/18] Fix tables numbers --- .../kotlin/org/jeudego/pairgoth/api/PairingHandler.kt | 4 +++- .../main/kotlin/org/jeudego/pairgoth/model/Pairing.kt | 5 +++-- .../kotlin/org/jeudego/pairgoth/model/Tournament.kt | 11 +++++++++-- .../org/jeudego/pairgoth/pairing/BasePairingHelper.kt | 7 ------- .../org/jeudego/pairgoth/pairing/solver/BaseSolver.kt | 2 +- .../jeudego/pairgoth/pairing/solver/MacMahonSolver.kt | 4 +++- .../jeudego/pairgoth/pairing/solver/SwissSolver.kt | 7 +++++-- 7 files changed, 24 insertions(+), 16 deletions(-) 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 a9ba349..808ae7c 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 @@ -23,7 +23,9 @@ object PairingHandler: PairgothApiHandler { }.toSet() 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 + 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, 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 9f52d14..ce3e152 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/Tournament.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt index fc2e116..f3adff8 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 @@ -7,6 +7,7 @@ import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.pairing.solver.MacMahonSolver import org.jeudego.pairgoth.pairing.solver.SwissSolver import org.jeudego.pairgoth.store.Store +import java.util.* import kotlin.math.roundToInt sealed class Tournament ( @@ -68,13 +69,19 @@ sealed class Tournament ( else mutableMapOf().also { games.add(it) } fun lastRound() = games.size + fun usedTables(round: Int): BitSet = + games(round).values.map { it.table }.fold(BitSet()) { acc, table -> + acc.set(table) + acc + } + fun recomputeDUDD(round: Int, gameID: ID) { // Instantiate solver with game history // TODO cleaner solver instantiation val history = games.map { games -> games.values.toList() } val solver = when (pairing.type) { - PairingType.SWISS -> SwissSolver(round, history, pairables.values.toList(), pairing.pairingParams, pairing.placementParams) - PairingType.MAC_MAHON -> MacMahonSolver(round, history, pairables.values.toList(), pairing.pairingParams, pairing.placementParams, mmBar = 3, mmFloor = -20) + PairingType.SWISS -> SwissSolver(round, history, pairables.values.toList(), pairing.pairingParams, pairing.placementParams, usedTables(round)) + PairingType.MAC_MAHON -> MacMahonSolver(round, history, pairables.values.toList(), pairing.pairingParams, pairing.placementParams, usedTables(round), mmBar = 3, mmFloor = -20) else -> throw Exception("Invalid tournament type") } // Recomputes DUDD 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 cba11c2..eef2a3f 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 @@ -134,11 +134,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 f49d819..c6c262b 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 @@ -26,6 +26,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 { @@ -532,7 +533,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 = pairing.handicap.handicap(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 2c35af3..8cb37a2 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,8 +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 { pairablesMap.mapValues { 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 From a8bf1e891d86c6adf937abdd6270fe09515578cf Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 20 Jan 2024 12:47:52 +0100 Subject: [PATCH 12/18] Allow edition of table number (but without recalc for now) --- .../main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 808ae7c..c41e81f 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 @@ -89,6 +89,10 @@ object PairingHandler: PairgothApiHandler { 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") + if (payload.containsKey("t")) { + // TODO CB - update *all* tables numbers accordingly + game.table = payload.getString("t")?.toIntOrNull() ?: badRequest("invalid table number") + } tournament.dispatchEvent(gameUpdated, Json.Object("round" to round, "game" to game.toJson())) return Json.Object("success" to true) } From 8c719d2fc7ff37401ddfe4a5f341381e34debfcc Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 20 Jan 2024 12:57:58 +0100 Subject: [PATCH 13/18] Version number parameterization in progress --- api-webapp/pom.xml | 1 + version.sh | 3 +++ 2 files changed, 4 insertions(+) create mode 100755 version.sh 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/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 From fdf39612ffb0c073bfc4cbe7885e78c36ff61883 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sun, 21 Jan 2024 16:37:19 +0100 Subject: [PATCH 14/18] Persistent dialog state and recap for registration, some fixes for printing pairing --- .../org/jeudego/pairgoth/view/PairgothTool.kt | 2 + view-webapp/src/main/sass/main.scss | 34 ++++++++++++++-- view-webapp/src/main/sass/tour.scss | 10 +++++ .../src/main/webapp/WEB-INF/translations/fr | 1 + view-webapp/src/main/webapp/js/main.js | 1 + .../main/webapp/js/tour-registration.inc.js | 40 +++++++++++-------- .../main/webapp/tour-registration.inc.html | 11 +++-- 7 files changed, 75 insertions(+), 24 deletions(-) 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..18ebe20 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 diff --git a/view-webapp/src/main/sass/main.scss b/view-webapp/src/main/sass/main.scss index 7859971..53b3511 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; @@ -385,11 +403,11 @@ } #title { - font-size: 1rem; - margin-top: 0; + font-size: 1rem !important; + margin-top: 0.1em !important; } - #logo, #lang, .steps, #filter-box, #footer, #pairing-left, #pairing-buttons, button, #standings-params { + #logo, #lang, .steps, #filter-box, #footer, #pairing-left, #pairing-buttons, button, #standings-params, #logout { display: none !important; } @@ -397,8 +415,16 @@ transform: scale(0.7); } + #pairing-right { + max-width: unset !important; + } + #paired { - max-height: unset; + max-height: unset !important; + max-width: unset !important; + font-size: 1rem !important; + line-height: 1.1rem !important; + min-width: 60vw; } } diff --git a/view-webapp/src/main/sass/tour.scss b/view-webapp/src/main/sass/tour.scss index 590ba9c..7d7d6d6 100644 --- a/view-webapp/src/main/sass/tour.scss +++ b/view-webapp/src/main/sass/tour.scss @@ -54,6 +54,13 @@ /* 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; @@ -229,6 +236,9 @@ background-color: rgba(100,200,255,200); cursor: grab; } + &:not(.selected):nth-child(2n) { + background-color: rgba(0,0,50,.1) + } } } #pairables { diff --git a/view-webapp/src/main/webapp/WEB-INF/translations/fr b/view-webapp/src/main/webapp/WEB-INF/translations/fr index bef15cd..5802fc6 100644 --- a/view-webapp/src/main/webapp/WEB-INF/translations/fr +++ b/view-webapp/src/main/webapp/WEB-INF/translations/fr @@ -146,3 +146,4 @@ to à unpairable players joueurs non disponibles version 0.1 supports the version 0.1 supporte le système d’appariement white vs. black blanc vs. Noir +confirmed. confirmé(s). \ No newline at end of file diff --git a/view-webapp/src/main/webapp/js/main.js b/view-webapp/src/main/webapp/js/main.js index 8a17e8c..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) { 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 f8eaa06..64394fd 100644 --- a/view-webapp/src/main/webapp/js/tour-registration.inc.js +++ b/view-webapp/src/main/webapp/js/tour-registration.inc.js @@ -84,6 +84,26 @@ function fillPlayer(player) { $('#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(() => { @@ -125,22 +145,7 @@ onLoad(() => { }); $('#add').on('click', e => { - 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(); + addPlayers(); }); $('#cancel-register').on('click', e => { e.preventDefault(); @@ -309,4 +314,7 @@ onLoad(() => { $('#filter')[0].value = ''; $('tbody > tr').removeClass('hidden'); }); + if (store('addingPlayers')) { + addPlayers(); + } }); diff --git a/view-webapp/src/main/webapp/tour-registration.inc.html b/view-webapp/src/main/webapp/tour-registration.inc.html index e9c278a..067d86f 100644 --- a/view-webapp/src/main/webapp/tour-registration.inc.html +++ b/view-webapp/src/main/webapp/tour-registration.inc.html @@ -1,15 +1,18 @@ +#set($parts = $api.get("tour/${params.id}/part")) +#set($pmap = $utils.toMap($parts))
-
+
+
+ $parts.size() participants, $utils.countFinals($parts) confirmed. +
-#set($parts = $api.get("tour/${params.id}/part")) -#set($pmap = $utils.toMap($parts))
Reg Name First name Country
+ + + $part.name $part.firstname$part.country$part.country.toUpperCase() $part.club #rank($part.rank) $part.rating result
Club RankRatingParticipationRatingParticipation
$part.num $part.place $part.name $part.firstname#rank($part.rank)#rank($part.rank) $part.country $number.format('0.#', $part.NBW)
@@ -175,7 +178,7 @@ $round +#set($stats = $utils.getResultsStats($games)) + $stats.known / $stats.total
Reg
@@ -23,7 +25,7 @@ - + #end #end From 2a733f7cdbcdc03b8e5afbb65b53bca63acb44d1 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Tue, 23 Jan 2024 12:15:03 +0100 Subject: [PATCH 16/18] Fix BIP unpairing --- .../main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 acb1650..98e20ec 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 @@ -73,7 +73,11 @@ 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) badRequest("Game already has a result"); + 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.recomputeDUDD(round, game.id) From b7e2e418ef23794867f9337de2541d9a1c4594c7 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Tue, 23 Jan 2024 13:39:50 +0100 Subject: [PATCH 17/18] Adjust FFG ratings to match EGF ones --- .../kotlin/org/jeudego/pairgoth/ratings/FFGRatingsHandler.kt | 2 ++ 1 file changed, 2 insertions(+) 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 } } } From 829fbb764ac495442e14a60c79537e29d497f68f Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Tue, 23 Jan 2024 19:28:12 +0100 Subject: [PATCH 18/18] Add individual correctionMms field --- .../kotlin/org/jeudego/pairgoth/model/Pairable.kt | 11 +++++++---- .../jeudego/pairgoth/pairing/solver/MacMahonSolver.kt | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) 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 068c681..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,7 +7,7 @@ import java.util.* // Pairable -sealed class Pairable(val id: ID, val name: String, open val rating: Int, open val rank: Int, val final: Boolean) { +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 { const val MIN_RANK: Int = -30 // 30k const val MAX_RANK: Int = 8 // 9D @@ -71,8 +71,9 @@ class Player( rank: Int, override var country: String, override var club: String, - final: Boolean -): Pairable(id, name, rating, rank, final) { + 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() @@ -87,6 +88,7 @@ class Player( "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 } @@ -106,7 +108,8 @@ fun Player.Companion.fromJson(json: Json.Object, default: Player? = null) = Play 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"), - final = json.getBoolean("final") ?: default?.final ?: true + 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/pairing/solver/MacMahonSolver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt index 8cb37a2..931ed37 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 @@ -24,7 +24,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
#$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]