diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt index 6972288..582e528 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt @@ -9,7 +9,7 @@ private fun createStoreImplementation(): StoreImplementation { return when (val storeProperty = WebappManager.getProperty("store") ?: "memory") { "memory" -> MemoryStore() "file" -> { - val filePath = WebappManager.getMandatoryProperty("store.file.path") ?: "." + val filePath = WebappManager.getProperty("store.file.path") ?: "." FileStore(filePath) } else -> throw Error("unknown store: $storeProperty") diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/AGARatingsHandler.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/AGARatingsHandler.kt new file mode 100644 index 0000000..541c8cf --- /dev/null +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/AGARatingsHandler.kt @@ -0,0 +1,14 @@ +package org.jeudego.pairgoth.ratings + +import com.republicate.kson.Json +import java.net.URL + +object AGARatingsHandler: RatingsHandler(RatingsManager.Ratings.AGA) { + override val defaultURL: URL by lazy { + throw Error("No URL for AGA...") + } + override val active = false + override fun parsePayload(payload: String): Json.Array { + return Json.Array() + } +} diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/EGFRatingsHandler.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/EGFRatingsHandler.kt new file mode 100644 index 0000000..d56f42d --- /dev/null +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/EGFRatingsHandler.kt @@ -0,0 +1,28 @@ +package org.jeudego.pairgoth.ratings + +import com.republicate.kson.Json +import java.net.URL + +object EGFRatingsHandler: RatingsHandler(RatingsManager.Ratings.EGF) { + override val defaultURL = URL("https://www.europeangodatabase.eu/EGD/EGD_2_0/downloads/allworld_lp.html") + override fun parsePayload(payload: String): Json.Array { + return payload.lines().filter { + it.matches(Regex("\\s+\\d+(?!.*\\(undefined\\)|Anonymous).*")) + }.mapNotNullTo(Json.MutableArray()) { + val match = linePattern.matchEntire(it) + if (match == null) { + logger.error("could not parse line: $it") + null + } else { + val pairs = groups.map { + Pair(it, match.groups[it]?.value) + }.toTypedArray() + Json.Object(*pairs) + } + } + } + // 19574643 Abad Jahin FR 38GJ 20k -- 15 2 T200202B + var linePattern = + Regex("\\s+(?\\d{8})\\s+(?$atom+)\\s(?$atom+)?,?\\s+(?[A-Z]{2})\\s+(?\\S{1,4})\\s+(?[1-9][0-9]?[kdp])\\s+(?[1-9][0-9]?[kdp]|--)\\s+(?-?[0-9]+)\\s+(?[0-9]+)\\s+(?\\S+)\\s*") + val groups = arrayOf("egf", "name", "firstname", "country", "club", "grade", "rating") +} 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 new file mode 100644 index 0000000..5db4c70 --- /dev/null +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/FFGRatingsHandler.kt @@ -0,0 +1,33 @@ +package org.jeudego.pairgoth.ratings + +import com.republicate.kson.Json +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import java.net.URL +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets + +object FFGRatingsHandler: RatingsHandler(RatingsManager.Ratings.FFG) { + override val defaultURL = URL("https://ffg.jeudego.org/echelle/echtxt/ech_ffg_V3.txt") + override fun parsePayload(payload: String): Json.Array { + return payload.lines().mapNotNullTo(Json.MutableArray()) { + val match = linePattern.matchEntire(it) + if (match == null) { + logger.error("could not parse line: $it") + null + } else { + val pairs = groups.map { + Pair(it, match.groups[it]?.value) + }.toTypedArray() + Json.Object(*pairs) + } + } + } + + override fun defaultCharset() = StandardCharsets.ISO_8859_1 + + var linePattern = + Regex("(?$atom+(?:\\((?:$atom|[0-9])+\\))?)\\s(?$atom+(?:\\((?:$atom|[0-9])+\\))?)\\s+(?-?[0-9]+)\\s(?[-eCLX])\\s(?(?:\\d|[A-Z]){7}|-------)\\s(?xxxx|XXXX|\\d{2}[a-zA-Z0-9]{2})\\s(?[A-Z]{2})") + val groups = arrayOf("name", "firstname", "rating", "license", "club", "country") +} diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/RatingsHandler.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/RatingsHandler.kt new file mode 100644 index 0000000..1180be9 --- /dev/null +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/RatingsHandler.kt @@ -0,0 +1,62 @@ +package org.jeudego.pairgoth.ratings + +import com.republicate.kson.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jeudego.pairgoth.web.WebappManager +import org.slf4j.LoggerFactory +import java.net.URL +import java.nio.charset.StandardCharsets +import java.util.* +import java.util.concurrent.TimeUnit + +abstract class RatingsHandler(val origin: RatingsManager.Ratings) { + + private val delay = TimeUnit.HOURS.toMillis(1L) + private val client = OkHttpClient() + abstract val defaultURL: URL + open val active = true + val cacheFile = RatingsManager.path.resolve("${origin.name}.json").toFile() + lateinit var players: Json.Array + + val url: URL by lazy { + WebappManager.getProperty("ratings.${origin.name.lowercase(Locale.ROOT)}")?.let { URL(it) } ?: defaultURL + } + + fun updateIfNeeded() { + if (Date().time - cacheFile.lastModified() > delay) { + RatingsManager.logger.info("Updating $origin cache from $url") + val payload = fetchPayload() + players = parsePayload(payload).also { + val cachePayload = it.toString() + cacheFile.printWriter().use { out -> + out.println(cachePayload) + } + } + } else if (!this::players.isInitialized) { + players = Json.parse(cacheFile.readText())?.asArray() ?: Json.Array() + } + } + + fun fetchPlayers(): Json.Array { + updateIfNeeded() + return players + } + + protected fun fetchPayload(): String { + val request = Request.Builder() + .url(url) + .build() + + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) throw Error("Could not fetch $origin ratings: unexpected code $response") + val contentType = response.headers["Content-Type"]?.toMediaType() + return response.body!!.source().readString(contentType?.charset() ?: defaultCharset()) + } + } + open fun defaultCharset() = StandardCharsets.UTF_8 + abstract fun parsePayload(payload: String): Json.Array + val logger = LoggerFactory.getLogger(origin.name) + val atom = "[-._`'a-zA-ZÀ-ÿ]" +} diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/RatingsManager.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/RatingsManager.kt new file mode 100644 index 0000000..74a5dbf --- /dev/null +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/RatingsManager.kt @@ -0,0 +1,43 @@ +package org.jeudego.pairgoth.ratings + +import com.republicate.kson.Json +import org.jeudego.pairgoth.web.WebappManager +import org.slf4j.LoggerFactory +import java.nio.file.Path +import java.util.* + +object RatingsManager: Runnable { + + enum class Ratings { + AGA, + EGF, + FFG + } + + val ratingsHandlers by lazy { + mapOf( + Pair(Ratings.AGA, AGARatingsHandler), + Pair(Ratings.EGF, EGFRatingsHandler), + Pair(Ratings.FFG, FFGRatingsHandler) + ); + } + + val timer = Timer() + lateinit var players: Json.MutableArray + override fun run() { + logger.info("launching ratings manager") + timer.scheduleAtFixedRate(Task, 0L, 3600000L) + } + object Task: TimerTask() { + override fun run() { + players = ratingsHandlers.values.filter { it.active }.flatMapTo(Json.MutableArray()) { ratings -> + ratings.fetchPlayers() + } + } + } + val logger = LoggerFactory.getLogger("ratings") + val path = Path.of(WebappManager.getProperty("ratings.path") ?: "ratings").also { + val file = it.toFile() + if (!file.mkdirs() && !file.isDirectory) throw Error("Property pairgoth.ratings.path must be a directory") + } +} diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/WebappManager.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/WebappManager.kt index fbc37b0..e26e556 100644 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/WebappManager.kt +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/WebappManager.kt @@ -1,15 +1,13 @@ package org.jeudego.pairgoth.web -import com.republicate.mailer.SmtpLoop import org.apache.commons.lang3.tuple.Pair +import org.jeudego.pairgoth.ratings.RatingsManager import org.jeudego.pairgoth.util.Translator import org.slf4j.LoggerFactory import java.io.IOException -import java.lang.IllegalAccessError import java.security.SecureRandom import java.security.cert.X509Certificate import java.util.* -import java.util.IllegalFormatCodePointException import javax.net.ssl.* import javax.servlet.* import javax.servlet.annotation.WebListener @@ -53,6 +51,7 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H override fun contextInitialized(sce: ServletContextEvent) { context = sce.servletContext logger.info("---------- Starting $WEBAPP_NAME ----------") + logger.info("info level is active") logger.debug("debug level is active") logger.trace("trace level is active") webappRoot = context.getRealPath("/") @@ -78,6 +77,9 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H // fail to correctly implement SSL... disableSSLCertificateChecks() + registerService("ratings", RatingsManager) + startService("ratings") + } catch (ioe: IOException) { logger.error("webapp initialization error", ioe) } diff --git a/view-webapp/src/main/sass/main.scss b/view-webapp/src/main/sass/main.scss index 11f97af..f9e31f4 100644 --- a/view-webapp/src/main/sass/main.scss +++ b/view-webapp/src/main/sass/main.scss @@ -254,6 +254,7 @@ left: -50%; border-radius: 10px; padding: 0.5em 1em; + z-index: 100; } #success { diff --git a/view-webapp/src/main/webapp/js/main.js b/view-webapp/src/main/webapp/js/main.js index ace4781..16ae0b8 100644 --- a/view-webapp/src/main/webapp/js/main.js +++ b/view-webapp/src/main/webapp/js/main.js @@ -84,8 +84,8 @@ function exportCSV(filename, content) { document.body.removeChild(link); } -/* modals */ - +/* modals + NOT IN USE, see popup-related code. NodeList.prototype.modal = function(show) { this.item(0).modal(show); return this; @@ -101,11 +101,12 @@ Element.prototype.modal = function(show) { } return this; } + */ /* DOM helpers */ -function formValue(name) { - let ctl = $(`[name="${name}"]`)[0]; +HTMLFormElement.prototype.val = function(name) { + let ctl = this.find(`[name="${name}"]`)[0]; if (!ctl) { console.error(`unknown input name: ${name}`) } @@ -124,7 +125,7 @@ function formValue(name) { } console.error(`unhandled input tag or type for input ${name} (tag: ${tag}, type:${type}`); return null; -} +}; function msg(id) { let ctl = $(`#${id}`)[0]; 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 4c11c23..e6d7e45 100644 --- a/view-webapp/src/main/webapp/js/tour-information.inc.js +++ b/view-webapp/src/main/webapp/js/tour-information.inc.js @@ -16,12 +16,13 @@ onLoad(() => { }); $('#validate').on('click', e => { + let form = e.target.closest('form'); let valid = true; // validate required fields let required = ['name', 'shortName', 'startDate', 'endDate']; - if (!$('input[name="online"]')[0].checked) required.push('location') + if (!form.find('input[name="online"]')[0].checked) required.push('location') for (let name of required) { - let ctl = $(`input[name=${name}]`)[0]; + let ctl = form.find(`input[name=${name}]`)[0]; let val = ctl.value; if (val) { ctl.setCustomValidity(''); @@ -32,7 +33,7 @@ onLoad(() => { } if (!valid) return; // validate short_name - let shortNameCtl = $('input[name="shortName"]')[0]; + let shortNameCtl = form.find('input[name="shortName"]')[0]; let shortName = shortNameCtl.value; if (safeRegex.test(shortName)) { shortNameCtl.setCustomValidity(''); @@ -40,7 +41,8 @@ onLoad(() => { valid = false; shortNameCtl.setCustomValidity(msg('invalid_character')); } - if (!valid) return; + // if (!valid) return; + // ... }); for(let name of ['startDate', 'endDate']) { @@ -99,37 +101,38 @@ onLoad(() => { $('#tournament-infos').on('submit', e => { e.preventDefault(); + let form = e.target; let tour = { - name: formValue('name'), - shortName: formValue('shortName'), - startDate: parseDate(formValue('startDate')), - endDate: parseDate(formValue('endDate')), - type: formValue('type'), - rounds: formValue('rounds'), - country: formValue('country'), - online: formValue('online'), - location: formValue('online') ? "" : formValue('location'), + name: form.val('name'), + shortName: form.val('shortName'), + startDate: parseDate(form.val('startDate')), + endDate: parseDate(form.val('endDate')), + type: form.val('type'), + rounds: form.val('rounds'), + country: form.val('country'), + online: form.val('online'), + location: form.val('online') ? "" : form.val('location'), pairing: { - type: formValue('pairing'), - // mmFloor: formValue('mmFloor'), - mmBar: formValue('mmBar'), + type: form.val('pairing'), + // mmFloor: form.val('mmFloor'), + mmBar: form.val('mmBar'), main: { - firstSeed: formValue('firstSeed'), - secondSeed: formValue('secondSeed') + firstSeed: form.val('firstSeed'), + secondSeed: form.val('secondSeed') }, handicap: { - correction: formValue('correction'), - treshold: formValue('treshold') + correction: form.val('correction'), + treshold: form.val('treshold') } }, timeSystem: { - type: formValue('timeSystemType'), - mainTime: fromHMS(formValue('mainTime')), - increment: fromHMS(formValue('increment')), - maxTime: fromHMS(formValue('maxTime')), - byoyomi: fromHMS(formValue('byoyomi')), - periods: formValue('periods'), - stones: formValue('stones') + type: form.val('timeSystemType'), + mainTime: fromHMS(form.val('mainTime')), + increment: fromHMS(form.val('increment')), + maxTime: fromHMS(form.val('maxTime')), + byoyomi: fromHMS(form.val('byoyomi')), + periods: form.val('periods'), + stones: form.val('stones') } } console.log(tour); 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 b7ed768..1bb43dc 100644 --- a/view-webapp/src/main/webapp/js/tour-registration.inc.js +++ b/view-webapp/src/main/webapp/js/tour-registration.inc.js @@ -5,5 +5,39 @@ onLoad(() => { min: 0, max: 4000 }); - //$('') + $('#register').on('click', e => { + let form = e.target.closest('form'); + let required = ['name', 'firstname', 'country', 'club', 'rank', 'rating']; + for (let name of required) { + let ctl = form.find(`[name=${name}]`)[0]; + let val = ctl.value; + if (val) { + ctl.setCustomValidity(''); + } else { + valid = false; + ctl.setCustomValidity(msg('required_field')); + } + } + if (!valid) return; + }); + $('#player-form').on('submit', e => { + e.preventDefault(); + let form = e.target; + let player = { + name: form.val('name'), + firstname: form.val('firstname'), + rating: form.val('rating'), + rank: form.val('rank'), + country: form.val('country'), + club: form.val('club') + } + console.log(player); + api.postJson(`tour/${tour_id}/part`, player) + .then(player => { + console.log(player) + if (player !== 'error') { + window.location.reload(); + } + }); + }); }); diff --git a/view-webapp/src/main/webapp/tour-registration.inc.html b/view-webapp/src/main/webapp/tour-registration.inc.html index 55987a6..944c8d7 100644 --- a/view-webapp/src/main/webapp/tour-registration.inc.html +++ b/view-webapp/src/main/webapp/tour-registration.inc.html @@ -14,8 +14,8 @@ $parts