Parse EGF and FFG ratings
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
@@ -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+(?<egf>\\d{8})\\s+(?<name>$atom+)\\s(?<firstname>$atom+)?,?\\s+(?<country>[A-Z]{2})\\s+(?<club>\\S{1,4})\\s+(?<grade>[1-9][0-9]?[kdp])\\s+(?<promotion>[1-9][0-9]?[kdp]|--)\\s+(?<rating>-?[0-9]+)\\s+(?<nt>[0-9]+)\\s+(?<last>\\S+)\\s*")
|
||||
val groups = arrayOf("egf", "name", "firstname", "country", "club", "grade", "rating")
|
||||
}
|
@@ -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("(?<name>$atom+(?:\\((?:$atom|[0-9])+\\))?)\\s(?<firstname>$atom+(?:\\((?:$atom|[0-9])+\\))?)\\s+(?<rating>-?[0-9]+)\\s(?<license>[-eCLX])\\s(?<ffg>(?:\\d|[A-Z]){7}|-------)\\s(?<club>xxxx|XXXX|\\d{2}[a-zA-Z0-9]{2})\\s(?<country>[A-Z]{2})")
|
||||
val groups = arrayOf("name", "firstname", "rating", "license", "club", "country")
|
||||
}
|
@@ -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À-ÿ]"
|
||||
}
|
@@ -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")
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
@@ -254,6 +254,7 @@
|
||||
left: -50%;
|
||||
border-radius: 10px;
|
||||
padding: 0.5em 1em;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#success {
|
||||
|
@@ -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];
|
||||
|
@@ -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);
|
||||
|
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -14,8 +14,8 @@ $parts
|
||||
</div>
|
||||
<div id="player" class="popup">
|
||||
<div class="popup-body">
|
||||
<div class="popup-content">
|
||||
<form id="player-form" class="ui form edit">
|
||||
<form id="player-form" class="ui form edit">
|
||||
<div class="popup-content">
|
||||
<div class="four stackable fields">
|
||||
<div class="twelve wide field">
|
||||
<div class="ui icon input">
|
||||
@@ -54,7 +54,7 @@ $parts
|
||||
<div class="eight wide field">
|
||||
<label>Given name</label>
|
||||
<span class="info"></span>
|
||||
<input type="text" name="name" placeholder="first name"/>
|
||||
<input type="text" name="firstname" placeholder="first name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two stackable fields">
|
||||
@@ -72,7 +72,7 @@ $parts
|
||||
<div class="eight wide field">
|
||||
<label>Club</label>
|
||||
<span class="info"></span>
|
||||
<input type="text" name="name" placeholder="club"/>
|
||||
<input type="text" name="club" placeholder="club"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two stackable fields">
|
||||
@@ -90,18 +90,18 @@ $parts
|
||||
<input name="rating" type="text" class="numeric"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="popup-footer">
|
||||
<button class="ui gray right labeled icon floating close button">
|
||||
<i class="times icon"></i>
|
||||
Cancel
|
||||
</button>
|
||||
<button class="ui green right labeled icon floating add button">
|
||||
<i class="plus icon"></i>
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="popup-footer">
|
||||
<button class="ui gray right labeled icon floating close button">
|
||||
<i class="times icon"></i>
|
||||
Cancel
|
||||
</button>
|
||||
<button id="register" class="ui green right labeled icon floating button">
|
||||
<i class="plus icon"></i>
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
|
Reference in New Issue
Block a user