From e33bc995c6b06bd08f297abe42019054c632acc7 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Mon, 25 Dec 2023 12:26:08 +0100 Subject: [PATCH] Several bugfixes and finishing touches --- .../jeudego/pairgoth/api/StandingsHandler.kt | 2 +- .../org/jeudego/pairgoth/ext/OpenGotha.kt | 7 +- .../jeudego/pairgoth/pairing/HistoryHelper.kt | 5 +- view-webapp/pom.xml | 5 ++ .../org/jeudego/pairgoth/util/Upload.kt | 53 ++++++++++++ .../org/jeudego/pairgoth/view/ApiTool.kt | 4 + .../org/jeudego/pairgoth/web/ImportServlet.kt | 28 +++++++ .../jeudego/pairgoth/web/LanguageFilter.kt | 3 +- view-webapp/src/main/sass/index.scss | 8 ++ view-webapp/src/main/sass/main.scss | 6 ++ view-webapp/src/main/sass/tour.scss | 3 +- .../main/webapp/WEB-INF/layouts/standard.html | 2 +- view-webapp/src/main/webapp/WEB-INF/web.xml | 10 +++ view-webapp/src/main/webapp/index.html | 80 +++++++++++++++++-- view-webapp/src/main/webapp/js/main.js | 36 +++++++++ .../main/webapp/js/tour-registration.inc.js | 2 +- .../src/main/webapp/tour-standings.inc.html | 4 +- view-webapp/src/main/webapp/tour.html | 44 +++++----- 18 files changed, 264 insertions(+), 38 deletions(-) create mode 100644 view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/Upload.kt create mode 100644 view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/ImportServlet.kt create mode 100644 view-webapp/src/main/sass/index.scss 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 38bb876..00f1dc9 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 @@ -46,7 +46,7 @@ object StandingsHandler: PairgothApiHandler { RANK -> tournament.pairables.mapValues { it.value.rank } RATING -> tournament.pairables.mapValues { it.value.rating } NBW -> historyHelper.wins - MMS -> historyHelper.scores + MMS -> historyHelper.mms STS -> nullMap CPS -> nullMap 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 c4b37ec..b1a81b7 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 @@ -142,7 +142,10 @@ object OpenGotha { country = player.country, club = player.club ).also { - canonicMap.put("${player.name}${player.firstName}".uppercase(Locale.ENGLISH), it.id) + player.participating.toString().forEachIndexed { i,c -> + if (c == '0') it.skip.add(i + 1) + } + canonicMap.put("${player.name.replace(" ", "")}${player.firstName.replace(" ", "")}".uppercase(Locale.ENGLISH), it.id) } }.associateByTo(tournament.players) { it.id } val gamesPerRound = ogTournament.games.game.groupBy { @@ -155,7 +158,7 @@ object OpenGotha { black = canonicMap[game.blackPlayer] ?: throw Error("player not found: ${game.blackPlayer}"), white = canonicMap[game.whitePlayer] ?: throw Error("player not found: ${game.whitePlayer}"), handicap = game.handicap, - result = when (game.result) { + result = when (game.result.removeSuffix("_BYDEF")) { "RESULT_UNKNOWN" -> Game.Result.UNKNOWN "RESULT_WHITEWINS" -> Game.Result.WHITE "RESULT_BLACKWINS" -> Game.Result.BLACK diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/HistoryHelper.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/HistoryHelper.kt index 3e73e95..a97e8f5 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/HistoryHelper.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/HistoryHelper.kt @@ -15,7 +15,7 @@ open class HistoryHelper(protected val history: List>, scoresGetter: else -> 0.0 } - val scores by lazy { + private val scores by lazy { scoresGetter() } @@ -76,6 +76,9 @@ open class HistoryHelper(protected val history: List>, scoresGetter: } } + // define mms to be a synonym of scores + val mms by lazy { scores } + // SOS related functions given a score function val sos by lazy { (history.flatten().map { game -> diff --git a/view-webapp/pom.xml b/view-webapp/pom.xml index fc541b1..ba1edbd 100644 --- a/view-webapp/pom.xml +++ b/view-webapp/pom.xml @@ -161,6 +161,11 @@ ${servlet.api.version} provided + + commons-fileupload + commons-fileupload + 1.5 + org.eclipse.jetty diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/Upload.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/Upload.kt new file mode 100644 index 0000000..df64af9 --- /dev/null +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/Upload.kt @@ -0,0 +1,53 @@ +package org.jeudego.pairgoth.util + +import org.apache.commons.fileupload.FileItemIterator +import org.apache.commons.fileupload.FileItemStream +import org.apache.commons.fileupload.FileUploadException +import org.apache.commons.fileupload.servlet.ServletFileUpload +import org.apache.commons.fileupload.util.Streams +import org.apache.commons.lang3.StringUtils +import org.slf4j.LoggerFactory +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import javax.servlet.http.HttpServletRequest + +object Upload { + internal var logger = LoggerFactory.getLogger("upload") + const val SIZE_RANDOM = 20 + @Throws(IOException::class, FileUploadException::class) + fun handleFileUpload(request: HttpServletRequest): List> { + // Check that we have a file upload request + val isMultipart: Boolean = ServletFileUpload.isMultipartContent(request) + if (!isMultipart) { + throw IOException("multipart content expected") + } + val files = mutableListOf>() + + // Create a new file upload handler + val upload = ServletFileUpload() + val iter: FileItemIterator = upload.getItemIterator(request) + + // over all fields + while (iter.hasNext()) { + val item: FileItemStream = iter.next() + val name: String = item.fieldName + val stream: InputStream = item.openStream() + if (item.isFormField) { + // standard fields set into request attributes + request.setAttribute(name, Streams.asString(stream)) + } else { + val filename: String = item.name + if (StringUtils.isEmpty(filename)) { + // ignoring empty file + continue + } + val input: InputStream = item.openStream() + val bytes = ByteArrayOutputStream() + Streams.copy(input, bytes, true) + files.add(Pair(filename, bytes.toByteArray())) + } + } + return files + } +} \ No newline at end of file diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/ApiTool.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/ApiTool.kt index dabcb33..c73c23d 100644 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/ApiTool.kt +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/ApiTool.kt @@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory class ApiTool { companion object { const val JSON = "application/json" + const val XML = "application/xml" val apiRoot = System.getProperty("pairgoth.api.external.url")?.let { "${it.removeSuffix("/")}/" } ?: throw Error("no configured API url") val logger = LoggerFactory.getLogger("api") @@ -50,4 +51,7 @@ class ApiTool { fun delete(url: String, payload: Json? = null) = prepare(url) .delete(payload?.toRequestBody() ?: EMPTY_REQUEST) .process() + + fun post(url: String, xml: String) = + prepare(url).post(xml.toRequestBody(XML.toMediaType())).process() } diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/ImportServlet.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/ImportServlet.kt new file mode 100644 index 0000000..1a23e27 --- /dev/null +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/ImportServlet.kt @@ -0,0 +1,28 @@ +package org.jeudego.pairgoth.web + +import org.jeudego.pairgoth.util.Upload +import org.jeudego.pairgoth.view.ApiTool +import java.nio.charset.StandardCharsets +import javax.servlet.http.HttpServlet +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +class ImportServlet: HttpServlet() { + + private val api = ApiTool() + + override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) { + val uploads = Upload.handleFileUpload(req) + if (uploads.size != 1) resp.sendError(HttpServletResponse.SC_BAD_REQUEST) + else { + val xml = uploads.first().second.toString(StandardCharsets.UTF_8) + val apiResp = api.post("tour", xml) + if (apiResp.isObject && apiResp.asObject().getBoolean("success") == true) { + resp.contentType = "application/json; charset=UTF-8" + resp.writer.println(apiResp.toString()) + } else { + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR) + } + } + } +} diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LanguageFilter.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LanguageFilter.kt index 2581309..6cf76c8 100644 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LanguageFilter.kt +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LanguageFilter.kt @@ -54,7 +54,8 @@ class LanguageFilter : Filter { } else { // the request must be redirected val destination = if (askedLanguage != null) target else uri - response.sendRedirect("/${preferredLanguage}${destination}") + val query = request.queryString ?: "" + response.sendRedirect("/${preferredLanguage}${destination}${if (query.isEmpty()) "" else "?$query"}") } } } diff --git a/view-webapp/src/main/sass/index.scss b/view-webapp/src/main/sass/index.scss new file mode 100644 index 0000000..06bd27b --- /dev/null +++ b/view-webapp/src/main/sass/index.scss @@ -0,0 +1,8 @@ +@layer pairgoth { + .tournaments.section { + display: flex; + flex-flow: row wrap; + gap: 1em; + justify-content: center; + } +} diff --git a/view-webapp/src/main/sass/main.scss b/view-webapp/src/main/sass/main.scss index 20f48ad..8f91417 100644 --- a/view-webapp/src/main/sass/main.scss +++ b/view-webapp/src/main/sass/main.scss @@ -320,6 +320,7 @@ max-height: inherit; } .popup-footer { + margin-top: 1em; position: relative; text-align: justify; display: flex; @@ -343,4 +344,9 @@ pointer-events: all; cursor: pointer; } + + thead { + position: sticky; + top: 0; + } } diff --git a/view-webapp/src/main/sass/tour.scss b/view-webapp/src/main/sass/tour.scss index cf43577..973eb9e 100644 --- a/view-webapp/src/main/sass/tour.scss +++ b/view-webapp/src/main/sass/tour.scss @@ -81,6 +81,7 @@ position: relative; .toggle { cursor: pointer; + text-align: center; input { display: none; } @@ -94,7 +95,7 @@ height: 26px; border-radius: 18px; background-color: #F7D6A3; - display: flex; + display: inline-flex; align-items: center; padding-left: 5px; padding-right: 5px; diff --git a/view-webapp/src/main/webapp/WEB-INF/layouts/standard.html b/view-webapp/src/main/webapp/WEB-INF/layouts/standard.html index ebc0ae5..cbf0be4 100644 --- a/view-webapp/src/main/webapp/WEB-INF/layouts/standard.html +++ b/view-webapp/src/main/webapp/WEB-INF/layouts/standard.html @@ -30,7 +30,7 @@ *#