Merge from webview2
This commit is contained in:
@@ -59,10 +59,20 @@
|
||||
<configuration>
|
||||
<scan>0</scan>
|
||||
<httpConnector>
|
||||
<port>8085</port>
|
||||
<host>${pairgoth.api.host}</host>
|
||||
<port>${pairgoth.api.port}</port>
|
||||
</httpConnector>
|
||||
<systemProperties>
|
||||
<pairgoth.env>${pairgoth.env}</pairgoth.env>
|
||||
<pairgoth.api.external.url>${pairgoth.api.external.url}</pairgoth.api.external.url>
|
||||
<pairgoth.webapp.external.url>${pairgoth.webapp.external.url}</pairgoth.webapp.external.url>
|
||||
<pairgoth.store>${pairgoth.store}</pairgoth.store>
|
||||
<pairgoth.store.file.path>${pairgoth.store.file.path}</pairgoth.store.file.path>
|
||||
<pairgoth.logger.level>${pairgoth.logger.level}</pairgoth.logger.level>
|
||||
<pairgoth.logger.format>${pairgoth.logger.format}</pairgoth.logger.format>
|
||||
</systemProperties>
|
||||
<webApp>
|
||||
<contextPath>/api/</contextPath>
|
||||
<contextPath>${pairgoth.api.context}/</contextPath>
|
||||
</webApp>
|
||||
</configuration>
|
||||
</plugin>
|
||||
@@ -134,6 +144,12 @@
|
||||
<artifactId>jakarta.mail</artifactId>
|
||||
<version>1.6.7</version>
|
||||
</dependency>
|
||||
<!-- utils -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.12.0</version>
|
||||
</dependency>
|
||||
<!-- auth -->
|
||||
<dependency>
|
||||
<groupId>org.pac4j</groupId>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package org.jeudego.pairgoth.api
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.web.ApiException
|
||||
import org.jeudego.pairgoth.server.ApiException
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
@@ -3,7 +3,7 @@ package org.jeudego.pairgoth.api
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.model.Tournament
|
||||
import org.jeudego.pairgoth.store.Store
|
||||
import org.jeudego.pairgoth.web.Event
|
||||
import org.jeudego.pairgoth.server.Event
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
interface PairgothApiHandler: ApiHandler {
|
||||
|
@@ -6,7 +6,8 @@ import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||
import org.jeudego.pairgoth.model.getID
|
||||
import org.jeudego.pairgoth.model.toID
|
||||
import org.jeudego.pairgoth.model.toJson
|
||||
import org.jeudego.pairgoth.web.Event.*
|
||||
import org.jeudego.pairgoth.server.Event
|
||||
import org.jeudego.pairgoth.server.Event.*
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
@@ -15,10 +16,18 @@ object PairingHandler: PairgothApiHandler {
|
||||
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
|
||||
val tournament = getTournament(request)
|
||||
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
|
||||
if (round > tournament.lastRound() + 1) badRequest("invalid round: previous round has not been played")
|
||||
val playing = tournament.games(round).values.flatMap {
|
||||
listOf(it.black, it.white)
|
||||
}.toSet()
|
||||
return tournament.pairables.values.filter { !it.skip.contains(round) && !playing.contains(it.id) }.map { it.id }.toJsonArray()
|
||||
val unpairables = tournament.pairables.values.filter { it.skip.contains(round) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray()
|
||||
val pairables = tournament.pairables.values.filter { !it.skip.contains(round) && !playing.contains(it.id) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray()
|
||||
val games = tournament.games(round).values
|
||||
return Json.Object(
|
||||
"games" to games.map { it.toJson() }.toCollection(Json.MutableArray()),
|
||||
"pairables" to pairables,
|
||||
"unpairables" to unpairables
|
||||
)
|
||||
}
|
||||
|
||||
override fun post(request: HttpServletRequest): Json {
|
||||
@@ -54,9 +63,19 @@ object PairingHandler: PairgothApiHandler {
|
||||
// only allow last round (if players have not been paired in the last round, it *may* be possible to be more laxist...)
|
||||
if (round != tournament.lastRound()) badRequest("cannot edit pairings in other rounds but the last")
|
||||
val payload = getObjectPayload(request)
|
||||
val game = tournament.games(round)[payload.getInt("id")] ?: badRequest("invalid game id")
|
||||
val gameId = payload.getInt("id") ?: badRequest("invalid game id")
|
||||
val game = tournament.games(round)[gameId] ?: badRequest("invalid game id")
|
||||
val playing = (tournament.games(round).values).filter { it.id != gameId }.flatMap {
|
||||
listOf(it.black, it.white)
|
||||
}.toSet()
|
||||
game.black = payload.getID("b") ?: badRequest("missing black player id")
|
||||
game.white = payload.getID("w") ?: badRequest("missing white player id")
|
||||
val black = tournament.pairables[game.black] ?: badRequest("invalid black player id")
|
||||
val white = tournament.pairables[game.black] ?: badRequest("invalid white player id")
|
||||
if (black.skip.contains(round)) badRequest("black is not playing this round")
|
||||
if (white.skip.contains(round)) badRequest("white is not playing this round")
|
||||
if (playing.contains(black.id)) badRequest("black is already in another game")
|
||||
if (playing.contains(white.id)) badRequest("white is already in another game")
|
||||
if (payload.containsKey("h")) game.handicap = payload.getString("h")?.toIntOrNull() ?: badRequest("invalid handicap")
|
||||
tournament.dispatchEvent(gameUpdated, Json.Object("round" to round, "game" to game.toJson()))
|
||||
return Json.Object("success" to true)
|
||||
|
@@ -5,8 +5,8 @@ import com.republicate.kson.toJsonArray
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||
import org.jeudego.pairgoth.model.Player
|
||||
import org.jeudego.pairgoth.model.fromJson
|
||||
import org.jeudego.pairgoth.web.Event
|
||||
import org.jeudego.pairgoth.web.Event.*
|
||||
import org.jeudego.pairgoth.server.Event
|
||||
import org.jeudego.pairgoth.server.Event.*
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
|
@@ -5,7 +5,7 @@ import com.republicate.kson.toJsonArray
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||
import org.jeudego.pairgoth.model.Game
|
||||
import org.jeudego.pairgoth.model.toJson
|
||||
import org.jeudego.pairgoth.web.Event
|
||||
import org.jeudego.pairgoth.server.Event
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
|
@@ -4,8 +4,8 @@ import com.republicate.kson.Json
|
||||
import com.republicate.kson.toJsonArray
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||
import org.jeudego.pairgoth.model.TeamTournament
|
||||
import org.jeudego.pairgoth.web.Event
|
||||
import org.jeudego.pairgoth.web.Event.*
|
||||
import org.jeudego.pairgoth.server.Event
|
||||
import org.jeudego.pairgoth.server.Event.*
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
|
@@ -10,9 +10,9 @@ import org.jeudego.pairgoth.model.Tournament
|
||||
import org.jeudego.pairgoth.model.fromJson
|
||||
import org.jeudego.pairgoth.model.toJson
|
||||
import org.jeudego.pairgoth.store.Store
|
||||
import org.jeudego.pairgoth.web.ApiServlet
|
||||
import org.jeudego.pairgoth.web.Event
|
||||
import org.jeudego.pairgoth.web.Event.*
|
||||
import org.jeudego.pairgoth.server.ApiServlet
|
||||
import org.jeudego.pairgoth.server.Event
|
||||
import org.jeudego.pairgoth.server.Event.*
|
||||
import org.w3c.dom.Element
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
@@ -64,8 +64,7 @@ object TournamentHandler: PairgothApiHandler {
|
||||
clear()
|
||||
putAll(tournament.games(round))
|
||||
}
|
||||
Store.replaceTournament(updated)
|
||||
tournament.dispatchEvent(tournamentUpdated, tournament.toJson())
|
||||
updated.dispatchEvent(tournamentUpdated, updated.toJson())
|
||||
return Json.Object("success" to true)
|
||||
}
|
||||
|
||||
|
@@ -151,6 +151,7 @@ object OpenGotha {
|
||||
it.value.map { game ->
|
||||
Game(
|
||||
id = Store.nextGameId,
|
||||
table = game.tableNumber,
|
||||
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,
|
||||
|
@@ -6,6 +6,7 @@ import java.util.*
|
||||
|
||||
data class Game(
|
||||
val id: ID,
|
||||
var table: Int,
|
||||
var white: ID,
|
||||
var black: ID,
|
||||
var handicap: Int = 0,
|
||||
@@ -33,6 +34,7 @@ data class Game(
|
||||
|
||||
fun Game.toJson() = Json.Object(
|
||||
"id" to id,
|
||||
"t" to table,
|
||||
"w" to white,
|
||||
"b" to black,
|
||||
"h" to handicap,
|
||||
@@ -42,6 +44,7 @@ fun Game.toJson() = Json.Object(
|
||||
|
||||
fun Game.Companion.fromJson(json: Json.Object) = Game(
|
||||
id = json.getID("id") ?: throw Error("missing game id"),
|
||||
table = json.getInt("t") ?: throw Error("missing game table"),
|
||||
white = json.getID("w") ?: throw Error("missing white player"),
|
||||
black = json.getID("b") ?: throw Error("missing black player"),
|
||||
handicap = json.getInt("h") ?: 0,
|
||||
|
@@ -3,6 +3,7 @@ package org.jeudego.pairgoth.model
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||
import org.jeudego.pairgoth.store.Store
|
||||
import java.util.*
|
||||
|
||||
// Pairable
|
||||
|
||||
@@ -51,6 +52,13 @@ fun Pairable.Companion.parseRank(rankStr: String): Int {
|
||||
|
||||
// Player
|
||||
|
||||
enum class DatabaseId {
|
||||
AGA,
|
||||
EGF,
|
||||
FFG;
|
||||
val key get() = this.name.lowercase(Locale.ROOT)
|
||||
}
|
||||
|
||||
class Player(
|
||||
id: ID,
|
||||
name: String,
|
||||
@@ -62,7 +70,7 @@ class Player(
|
||||
): Pairable(id, name, rating, rank) {
|
||||
companion object
|
||||
// used to store external IDs ("FFG" => FFG ID, "EGF" => EGF PIN, "AGA" => AGA ID ...)
|
||||
val externalIds = mutableMapOf<String, String>()
|
||||
val externalIds = mutableMapOf<DatabaseId, String>()
|
||||
override fun toJson(): Json.Object = Json.MutableObject(
|
||||
"id" to id,
|
||||
"name" to name,
|
||||
@@ -71,8 +79,11 @@ class Player(
|
||||
"rank" to rank,
|
||||
"country" to country,
|
||||
"club" to club
|
||||
).also {
|
||||
if (skip.isNotEmpty()) it["skip"] = Json.Array(skip)
|
||||
).also { json ->
|
||||
if (skip.isNotEmpty()) json["skip"] = Json.Array(skip)
|
||||
externalIds.forEach { (dbid, id) ->
|
||||
json[dbid.key] = id
|
||||
}
|
||||
}
|
||||
override fun nameSeed(separator: String): String {
|
||||
return name + separator + firstname
|
||||
@@ -92,4 +103,9 @@ fun Player.Companion.fromJson(json: Json.Object, default: Player? = null) = Play
|
||||
json.getArray("skip")?.let {
|
||||
if (it.isNotEmpty()) player.skip.addAll(it.map { id -> (id as Number).toInt() })
|
||||
}
|
||||
DatabaseId.values().forEach { dbid ->
|
||||
json.getString(dbid.key)?.let { id ->
|
||||
player.externalIds[dbid] = id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ data class MainCritParams(
|
||||
val additionalPlacementCritSystem1: Criterion = Criterion.RATING,
|
||||
val additionalPlacementCritSystem2: Criterion = Criterion.NONE,
|
||||
) {
|
||||
enum class DrawUpDown {TOP, MIDDLE, BOTTOM}
|
||||
enum class DrawUpDown { TOP, MIDDLE, BOTTOM }
|
||||
enum class SeedMethod { SPLIT_AND_FOLD, SPLIT_AND_RANDOM, SPLIT_AND_SLIP }
|
||||
companion object {
|
||||
const val MAX_CATEGORIES_WEIGHT = 20000000000000.0 // 2e13
|
||||
@@ -185,7 +185,7 @@ class MacMahon(
|
||||
handicap = HandicapParams(
|
||||
weight = MainCritParams.MAX_SCORE_WEIGHT, // TODO - contradictory with the comment above (not used anyway ?!)
|
||||
useMMS = true,
|
||||
rankThreshold = 0,
|
||||
rankThreshold = -20,
|
||||
ceiling = 9
|
||||
)
|
||||
),
|
||||
|
@@ -3,7 +3,7 @@ package org.jeudego.pairgoth.oauth
|
||||
// In progress
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.web.WebappManager
|
||||
import org.jeudego.pairgoth.server.WebappManager
|
||||
//import com.republicate.modality.util.AESCryptograph
|
||||
//import com.republicate.modality.util.Cryptograph
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
@@ -21,7 +21,7 @@ abstract class OAuthHelper {
|
||||
protected get() = WebappManager.getMandatoryProperty("oauth." + name + ".secret")
|
||||
protected val redirectURI: String?
|
||||
protected get() = try {
|
||||
val uri: String = WebappManager.Companion.getProperty("webapp.url") + "/oauth.html"
|
||||
val uri: String = WebappManager.getProperty("webapp.external.url") + "/oauth.html"
|
||||
URLEncoder.encode(uri, "UTF-8")
|
||||
} catch (uee: UnsupportedEncodingException) {
|
||||
logger.error("could not encode redirect URI", uee)
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package org.jeudego.pairgoth.pairing
|
||||
|
||||
import org.jeudego.pairgoth.model.*
|
||||
import java.util.*
|
||||
|
||||
abstract class BasePairingHelper(
|
||||
history: List<List<Game>>, // History of all games played for each round
|
||||
@@ -131,4 +132,11 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
@@ -28,7 +28,7 @@ sealed class BaseSolver(
|
||||
|
||||
companion object {
|
||||
val rand = Random(/* seed from properties - TODO */)
|
||||
val DEBUG_EXPORT_WEIGHT = true
|
||||
val DEBUG_EXPORT_WEIGHT = false
|
||||
var byePlayers: MutableList<Pairable> = mutableListOf()
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ sealed class BaseSolver(
|
||||
|
||||
var result = sorted.flatMap { games(white = it[0], black = it[1]) }
|
||||
// add game for ByePlayer
|
||||
if (chosenByePlayer != ByePlayer) result += Game(id = Store.nextGameId, white = ByePlayer.id, black = chosenByePlayer.id, result = Game.Result.fromSymbol('b'))
|
||||
if (chosenByePlayer != ByePlayer) result += Game(id = Store.nextGameId, table = 0, white = ByePlayer.id, black = chosenByePlayer.id, result = Game.Result.fromSymbol('b'))
|
||||
|
||||
if (DEBUG_EXPORT_WEIGHT) {
|
||||
//println("DUDD debug")
|
||||
@@ -535,7 +535,9 @@ sealed class BaseSolver(
|
||||
|
||||
open fun games(black: Pairable, white: Pairable): List<Game> {
|
||||
// CB TODO team of individuals pairing
|
||||
|
||||
return listOf(Game(id = Store.nextGameId, black = black.id, white = white.id, handicap = pairing.handicap.handicap(black, white), drawnUpDown = white.group-black.group))
|
||||
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 = white.group-black.group))
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,36 @@
|
||||
package org.jeudego.pairgoth.server
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import java.io.IOException
|
||||
|
||||
class ApiException : IOException {
|
||||
var code: Int
|
||||
private set
|
||||
var details: Json.Object
|
||||
private set
|
||||
|
||||
constructor(code: Int) : super("error") {
|
||||
this.code = code
|
||||
details = Json.Object("message" to message)
|
||||
}
|
||||
|
||||
constructor(code: Int, message: String?) : super(message) {
|
||||
this.code = code
|
||||
details = Json.Object("message" to message)
|
||||
}
|
||||
|
||||
constructor(code: Int, cause: Exception) : super(cause) {
|
||||
this.code = code
|
||||
details = Json.Object("message" to "Erreur interne du serveur : " + cause.message)
|
||||
}
|
||||
|
||||
constructor(code: Int, message: String, cause: Exception) : super(message, cause) {
|
||||
this.code = code
|
||||
details = Json.Object("message" to message + " : " + cause.message)
|
||||
}
|
||||
|
||||
constructor(code: Int, details: Json.Object) : super(details.getString("message")) {
|
||||
this.code = code
|
||||
this.details = details
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package org.jeudego.pairgoth.web
|
||||
package org.jeudego.pairgoth.server
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.api.ApiHandler
|
||||
@@ -23,7 +23,7 @@ import javax.servlet.http.HttpServlet
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
class ApiServlet : HttpServlet() {
|
||||
class ApiServlet: HttpServlet() {
|
||||
|
||||
public override fun doGet(request: HttpServletRequest, response: HttpServletResponse) {
|
||||
doRequest(request, response)
|
||||
@@ -62,7 +62,7 @@ class ApiServlet : HttpServlet() {
|
||||
|
||||
// validate request
|
||||
|
||||
if ("dev" == WebappManager.getProperty("webapp.env")) {
|
||||
if ("dev" == WebappManager.getProperty("env")) {
|
||||
response.addHeader("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
validateAccept(request);
|
||||
@@ -90,7 +90,7 @@ class ApiServlet : HttpServlet() {
|
||||
"team" -> TeamHandler
|
||||
else -> ApiHandler.badRequest("unknown sub-entity: $subEntity")
|
||||
}
|
||||
"player" -> PlayerHandler
|
||||
// "player" -> PlayerHandler
|
||||
else -> ApiHandler.badRequest("unknown entity: $entity")
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ class ApiServlet : HttpServlet() {
|
||||
cause: Throwable? = null
|
||||
) {
|
||||
try {
|
||||
if (code == 500) {
|
||||
if (code == 500 || response.isCommitted) {
|
||||
logger.error(
|
||||
"Request {} {} gave error {} {}",
|
||||
request.method,
|
||||
@@ -245,7 +245,14 @@ class ApiServlet : HttpServlet() {
|
||||
cause
|
||||
)
|
||||
}
|
||||
response.sendError(code, message)
|
||||
response.status = code
|
||||
if (response.isCommitted) return
|
||||
val errorPayload = Json.Object(
|
||||
"success" to false,
|
||||
"error" to (message ?: "unknown error")
|
||||
)
|
||||
setContentType(response)
|
||||
errorPayload.toString(response.writer)
|
||||
} catch (ioe: IOException) {
|
||||
logger.error("Could not send back error", ioe)
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package org.jeudego.pairgoth.web
|
||||
package org.jeudego.pairgoth.server
|
||||
|
||||
import info.macias.sse.events.MessageEvent
|
||||
import java.util.concurrent.atomic.AtomicLong
|
@@ -1,4 +1,4 @@
|
||||
package org.jeudego.pairgoth.web
|
||||
package org.jeudego.pairgoth.server
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.util.Colorizer.blue
|
||||
@@ -15,9 +15,11 @@ fun Logger.logRequest(req: HttpServletRequest, logHeaders: Boolean = false) {
|
||||
.append(req.localName)
|
||||
val port = req.localPort
|
||||
if (port != 80) builder.append(':').append(port)
|
||||
/*
|
||||
if (!req.contextPath.isEmpty()) {
|
||||
builder.append(req.contextPath)
|
||||
}
|
||||
*/
|
||||
builder.append(req.requestURI)
|
||||
if (req.method == "GET") {
|
||||
val qs = req.queryString
|
@@ -1,4 +1,4 @@
|
||||
package org.jeudego.pairgoth.web
|
||||
package org.jeudego.pairgoth.server
|
||||
|
||||
import info.macias.sse.EventBroadcast
|
||||
import info.macias.sse.events.MessageEvent
|
@@ -1,4 +1,4 @@
|
||||
package org.jeudego.pairgoth.web
|
||||
package org.jeudego.pairgoth.server
|
||||
|
||||
import com.republicate.mailer.SmtpLoop
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
@@ -51,8 +51,7 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
|
||||
/* ServletContextListener interface */
|
||||
override fun contextInitialized(sce: ServletContextEvent) {
|
||||
context = sce.servletContext
|
||||
logger.info("---------- Starting Pairgoth Server ----------")
|
||||
context.setAttribute("manager", this)
|
||||
logger.info("---------- Starting $WEBAPP_NAME ----------")
|
||||
webappRoot = context.getRealPath("/")
|
||||
try {
|
||||
// load default properties
|
||||
@@ -63,7 +62,8 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
|
||||
properties[(key as String).removePrefix(PAIRGOTH_PROPERTIES_PREFIX)] = value
|
||||
}
|
||||
|
||||
logger.info("Using profile {}", properties.getProperty("webapp.env"))
|
||||
val env = properties.getProperty("env")
|
||||
logger.info("Using profile $env", )
|
||||
|
||||
// set system user agent string to empty string
|
||||
System.setProperty("http.agent", "")
|
||||
@@ -84,11 +84,15 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
|
||||
}
|
||||
|
||||
override fun contextDestroyed(sce: ServletContextEvent) {
|
||||
logger.info("---------- Stopping Web Application ----------")
|
||||
logger.info("---------- Stopping $WEBAPP_NAME ----------")
|
||||
|
||||
stopService("smtp");
|
||||
|
||||
val context = sce.servletContext
|
||||
for (service in webServices.keys) stopService(service, true)
|
||||
// ??? DriverManager.deregisterDriver(com.mysql.cj.jdbc.Driver ...);
|
||||
|
||||
logger.info("---------- Stopped $WEBAPP_NAME ----------")
|
||||
}
|
||||
|
||||
/* ServletContextAttributeListener interface */
|
||||
@@ -101,6 +105,7 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
|
||||
override fun sessionDestroyed(se: HttpSessionEvent) {}
|
||||
|
||||
companion object {
|
||||
const val WEBAPP_NAME = "Pairgoth API Server"
|
||||
const val PAIRGOTH_PROPERTIES_PREFIX = "pairgoth."
|
||||
lateinit var webappRoot: String
|
||||
lateinit var context: ServletContext
|
||||
@@ -111,10 +116,10 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
|
||||
return properties.getProperty(prop)
|
||||
}
|
||||
fun getMandatoryProperty(prop: String): String {
|
||||
return properties.getProperty(prop) ?: throw Error("missing property: ${prop}")
|
||||
return getProperty(prop) ?: throw Error("missing property: ${prop}")
|
||||
}
|
||||
|
||||
val webappURL by lazy { getProperty("webapp.url") }
|
||||
val webappURL by lazy { getProperty("webapp.external.url") }
|
||||
|
||||
private val services = mutableMapOf<String, Pair<Runnable, Thread>>()
|
||||
|
@@ -10,7 +10,7 @@ import org.jeudego.pairgoth.model.fromJson
|
||||
import org.jeudego.pairgoth.model.getID
|
||||
import org.jeudego.pairgoth.model.toFullJson
|
||||
import org.jeudego.pairgoth.model.toID
|
||||
import org.jeudego.pairgoth.model.toJson
|
||||
import java.lang.Integer.max
|
||||
import java.nio.file.Path
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -64,11 +64,15 @@ class FileStore(pathStr: String): StoreImplementation {
|
||||
}
|
||||
val json = Json.parse(path.resolve(file).readText())?.asObject() ?: throw Error("could not read tournament")
|
||||
val tournament = Tournament.fromJson(json)
|
||||
var maxPlayerId = 0
|
||||
var maxGameId = 0
|
||||
val players = json["players"] as Json.Array? ?: Json.Array()
|
||||
tournament.players.putAll(
|
||||
players.associate {
|
||||
(it as Json.Object).let { player ->
|
||||
Pair(player.getID("id") ?: throw Error("invalid tournament file"), Player.fromJson(player))
|
||||
Pair(player.getID("id") ?: throw Error("invalid tournament file"), Player.fromJson(player)).also {
|
||||
maxPlayerId = max(maxPlayerId, it.first)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -77,29 +81,41 @@ class FileStore(pathStr: String): StoreImplementation {
|
||||
tournament.teams.putAll(
|
||||
teams.associate {
|
||||
(it as Json.Object).let { team ->
|
||||
Pair(team.getID("id") ?: throw Error("invalid tournament file"), tournament.teamFromJson(team))
|
||||
Pair(team.getID("id") ?: throw Error("invalid tournament file"), tournament.teamFromJson(team)).also {
|
||||
maxPlayerId = max(maxPlayerId, it.first)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
val games = json["games"] as Json.Array? ?: Json.Array()
|
||||
(1..games.size).forEach { round ->
|
||||
var nextDefaultTable = 1;
|
||||
val roundGames = games[round - 1] as Json.Array
|
||||
tournament.games(round).putAll(
|
||||
games.associate {
|
||||
roundGames.associate {
|
||||
(it as Json.Object).let { game ->
|
||||
Pair(game.getID("id") ?: throw Error("invalid tournament file"), Game.fromJson(game))
|
||||
val fixedGame =
|
||||
if (game.containsKey("t")) game
|
||||
else Json.MutableObject(game).set("t", nextDefaultTable++)
|
||||
Pair(game.getID("id") ?: throw Error("invalid tournament file"), Game.fromJson(fixedGame)).also {
|
||||
maxGameId = max(maxGameId, it.first)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
_nextPlayerId.set(maxPlayerId + 1)
|
||||
_nextGameId.set(maxGameId + 1)
|
||||
return tournament
|
||||
}
|
||||
|
||||
override fun replaceTournament(tournament: Tournament<*>) {
|
||||
val filename = tournament.filename()
|
||||
val file = path.resolve(filename).toFile()
|
||||
if (!file.exists()) throw Error("File $filename does not exist")
|
||||
file.renameTo(path.resolve(filename + "-${timestamp}").toFile())
|
||||
if (file.exists()) {
|
||||
file.renameTo(path.resolve(filename + "-${timestamp}").toFile())
|
||||
}
|
||||
addTournament(tournament)
|
||||
}
|
||||
|
||||
|
@@ -2,14 +2,14 @@ package org.jeudego.pairgoth.store
|
||||
|
||||
import org.jeudego.pairgoth.model.ID
|
||||
import org.jeudego.pairgoth.model.Tournament
|
||||
import org.jeudego.pairgoth.web.WebappManager
|
||||
import org.jeudego.pairgoth.server.WebappManager
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
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")
|
||||
|
@@ -1,6 +1,19 @@
|
||||
# webapp
|
||||
webapp.env = dev
|
||||
webapp.url = http://localhost:8080
|
||||
# environment
|
||||
env = dev
|
||||
|
||||
# webapp connector
|
||||
webapp.protocol = http
|
||||
webapp.interface = localhost
|
||||
webapp.port = 8080
|
||||
webapp.context = /
|
||||
webapp.external.url = http://localhost:8080
|
||||
|
||||
# api connector
|
||||
api.protocol = http
|
||||
api.interface = localhost
|
||||
api.port = 8085
|
||||
api.context = /api
|
||||
api.external.url = http://localhost:8085/api
|
||||
|
||||
# store
|
||||
store = file
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<listener-class>com.republicate.slf4j.impl.ServletContextLoggerListener</listener-class>
|
||||
</listener>
|
||||
<listener>
|
||||
<listener-class>org.jeudego.pairgoth.web.WebappManager</listener-class>
|
||||
<listener-class>org.jeudego.pairgoth.server.WebappManager</listener-class>
|
||||
</listener>
|
||||
|
||||
<!-- filters -->
|
||||
@@ -29,11 +29,11 @@
|
||||
<!-- servlets -->
|
||||
<servlet>
|
||||
<servlet-name>api</servlet-name>
|
||||
<servlet-class>org.jeudego.pairgoth.web.ApiServlet</servlet-class>
|
||||
<servlet-class>org.jeudego.pairgoth.server.ApiServlet</servlet-class>
|
||||
</servlet>
|
||||
<servlet>
|
||||
<servlet-name>sse</servlet-name>
|
||||
<servlet-class>org.jeudego.pairgoth.web.SSEServlet</servlet-class>
|
||||
<servlet-class>org.jeudego.pairgoth.server.SSEServlet</servlet-class>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
<async-supported>true</async-supported>
|
||||
</servlet>
|
||||
|
@@ -180,7 +180,7 @@ class BasicTests: TestBase() {
|
||||
"""[{"id":$aTournamentGameID,"w":$anotherPlayerID,"b":$aPlayerID,"h":0,"r":"?","dd":0}]"""
|
||||
)
|
||||
assertTrue(possibleResults.contains(games.toString()), "pairing differs")
|
||||
games = TestAPI.get("/api/tour/$aTournamentID/res/1").asArray()
|
||||
games = TestAPI.get("/api/tour/$aTournamentID/res/1").asObject().getArray("games")!!
|
||||
assertTrue(possibleResults.contains(games.toString()), "results differs")
|
||||
val empty = TestAPI.get("/api/tour/$aTournamentID/pair/1").asArray()
|
||||
assertEquals("[]", empty.toString(), "no more pairables for round 1")
|
||||
|
@@ -2,9 +2,9 @@ package org.jeudego.pairgoth.test
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.api.ApiHandler
|
||||
import org.jeudego.pairgoth.web.ApiServlet
|
||||
import org.jeudego.pairgoth.web.SSEServlet
|
||||
import org.jeudego.pairgoth.web.WebappManager
|
||||
import org.jeudego.pairgoth.server.ApiServlet
|
||||
import org.jeudego.pairgoth.server.SSEServlet
|
||||
import org.jeudego.pairgoth.server.WebappManager
|
||||
import org.mockito.kotlin.*
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
|
@@ -6,5 +6,5 @@ CSSWATCH=$!
|
||||
|
||||
export MAVEN_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5006"
|
||||
#mvn --projects view-webapp -Dpairgoth.api.url=http://localhost:8085/api/ package jetty:run
|
||||
mvn --projects view-webapp package jetty:run
|
||||
mvn -DskipTests=true --projects view-webapp package jetty:run
|
||||
kill $CSSWATCH
|
||||
|
101
pom.xml
101
pom.xml
@@ -8,7 +8,7 @@
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<!-- CB: Temporary add my repository, while waiting for SSE Java server module author to incorporate my PR or for me to fork it -->
|
||||
<!-- CB: Temporary add my repository, while waiting for SSE Java server module author to incorporate my PR or for me to fork it -->
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>central</id>
|
||||
@@ -19,7 +19,8 @@
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository> <repository>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>republicate.com</id>
|
||||
<url>https://republicate.com/maven2</url>
|
||||
<releases>
|
||||
@@ -31,6 +32,39 @@
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>manual-properties</id>
|
||||
<activation>
|
||||
<file>
|
||||
<exists>pairgoth.properties</exists>
|
||||
</file>
|
||||
</activation>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>properties-maven-plugin</artifactId>
|
||||
<version>1.2.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>initialize</phase>
|
||||
<goals>
|
||||
<goal>read-project-properties</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<files>
|
||||
<file>pairgoth.properties</file>
|
||||
</files>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
@@ -62,6 +96,28 @@
|
||||
<kotlin.code.style>official</kotlin.code.style>
|
||||
<kotlin.compiler.jvmTarget>10</kotlin.compiler.jvmTarget>
|
||||
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
|
||||
|
||||
<!-- pairgoth default properties -->
|
||||
<pairgoth.env>dev</pairgoth.env>
|
||||
<pairgoth.webapp.protocol>http</pairgoth.webapp.protocol>
|
||||
<pairgoth.webapp.host>localhost</pairgoth.webapp.host>
|
||||
<pairgoth.webapp.port>8080</pairgoth.webapp.port>
|
||||
<pairgoth.webapp.context>/</pairgoth.webapp.context>
|
||||
<pairgoth.webapp.external.url>http://localhost:8080</pairgoth.webapp.external.url>
|
||||
<pairgoth.api.protocol>http</pairgoth.api.protocol>
|
||||
<pairgoth.api.host>localhost</pairgoth.api.host>
|
||||
<pairgoth.api.port>8085</pairgoth.api.port>
|
||||
<pairgoth.api.context>/api</pairgoth.api.context>
|
||||
<pairgoth.api.external.url>http://localhost:8085/api</pairgoth.api.external.url>
|
||||
<pairgoth.store>file</pairgoth.store>
|
||||
<pairgoth.store.file.path>tournamentfiles</pairgoth.store.file.path>
|
||||
<pairgoth.smtp.sender></pairgoth.smtp.sender>
|
||||
<pairgoth.smtp.host></pairgoth.smtp.host>
|
||||
<pairgoth.smtp.port>587</pairgoth.smtp.port>
|
||||
<pairgoth.smtp.user></pairgoth.smtp.user>
|
||||
<pairgoth.smtp.password></pairgoth.smtp.password>
|
||||
<pairgoth.logger.level>info</pairgoth.logger.level>
|
||||
<pairgoth.logger.format>[%level] %ip [%logger] %message</pairgoth.logger.format>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
@@ -144,6 +200,47 @@
|
||||
<artifactId>maven-war-plugin</artifactId>
|
||||
<version>${maven.war.plugin.version}</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<version>3.4.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>regex-property</id>
|
||||
<goals>
|
||||
<goal>regex-property</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<name>webapp.port</name>
|
||||
<value>${webapp.url}</value>
|
||||
<regex>^.*:(\d+).*$</regex>
|
||||
<replacement>$1</replacement>
|
||||
<failIfNoMatch>true</failIfNoMatch>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>echo-property</id>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<echo>----------------- webapp.port=${webapp.port}</echo>
|
||||
</tasks>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.mycila</groupId>
|
||||
<artifactId>license-maven-plugin</artifactId>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
export MAVEN_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005"
|
||||
mvn --projects api-webapp package jetty:run
|
||||
mvn -DskipTests=true --projects api-webapp package jetty:run
|
||||
|
@@ -3,4 +3,4 @@
|
||||
# debug version
|
||||
# mvn package && java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5006 -jar application/target/pairgoth-engine.jar
|
||||
|
||||
mvn package && java -jar application/target/pairgoth-engine.jar
|
||||
mvn -DskipTests=true package && java -jar application/target/pairgoth-engine.jar
|
||||
|
@@ -17,6 +17,7 @@
|
||||
<url>TODO</url>
|
||||
<properties>
|
||||
<pac4j.version>5.7.1</pac4j.version>
|
||||
<lucene.version>9.9.0</lucene.version>
|
||||
</properties>
|
||||
<build>
|
||||
<defaultGoal>package</defaultGoal>
|
||||
@@ -68,44 +69,32 @@
|
||||
</executions>
|
||||
<configuration>
|
||||
<outputFolder>${project.build.directory}/generated-resources/css</outputFolder>
|
||||
<version>1.69.5</version>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!--
|
||||
<plugin>
|
||||
<groupId>com.gitlab.haynes</groupId>
|
||||
<artifactId>libsass-maven-plugin</artifactId>
|
||||
<version>0.2.29</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<inputPath>${basedir}/src/main/sass/</inputPath>
|
||||
<inputSyntax>scss</inputSyntax>
|
||||
<outputPath>${project.build.directory}/${project.build.finalName}/css</outputPath>
|
||||
<generateSourceMap>true</generateSourceMap>
|
||||
<sourceMapOutputPath>${project.build.directory}/${project.build.finalName}/css</sourceMapOutputPath>
|
||||
</configuration>
|
||||
</plugin>
|
||||
-->
|
||||
<plugin>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-maven-plugin</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
<configuration>
|
||||
<scan>1</scan>
|
||||
<scan>0</scan>
|
||||
<httpConnector>
|
||||
<port>8080</port>
|
||||
<host>${pairgoth.webapp.host}</host>
|
||||
<port>${pairgoth.webapp.port}</port>
|
||||
</httpConnector>
|
||||
<systemProperties>
|
||||
<pairgoth.api.url>http://localhost:8085/api/</pairgoth.api.url>
|
||||
<pairgoth.env>dev</pairgoth.env>
|
||||
<pairgoth.env>${pairgoth.env}</pairgoth.env>
|
||||
<pairgoth.api.external.url>${pairgoth.api.external.url}</pairgoth.api.external.url>
|
||||
<pairgoth.webapp.external.url>${pairgoth.webapp.external.url}</pairgoth.webapp.external.url>
|
||||
<pairgoth.store>${pairgoth.store}</pairgoth.store>
|
||||
<pairgoth.store.file.path>${pairgoth.store}</pairgoth.store.file.path>
|
||||
<pairgoth.logger.level>${pairgoth.logger.level}</pairgoth.logger.level>
|
||||
<pairgoth.logger.format>${pairgoth.logger.format}</pairgoth.logger.format>
|
||||
<pairgoth.logger.level.org.jeudego.pairgoth.web.ApiServlet.api>debug</pairgoth.logger.level.org.jeudego.pairgoth.web.ApiServlet.api>
|
||||
<org.slf4j.simpleLogger.defaultLogLevel>debug</org.slf4j.simpleLogger.defaultLogLevel>
|
||||
</systemProperties>
|
||||
<webApp>
|
||||
<contextPath>${pairgoth.webapp.context}/</contextPath>
|
||||
<resourceBases>${project.basedir}/src/main/webapp,${project.build.directory}/generated-resources/</resourceBases>
|
||||
</webApp>
|
||||
</configuration>
|
||||
@@ -125,6 +114,10 @@
|
||||
</webResources>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>properties-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencyManagement>
|
||||
@@ -168,6 +161,12 @@
|
||||
<version>${servlet.api.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- proxy -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-proxy</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
<artifactId>jakarta.mail</artifactId>
|
||||
@@ -249,6 +248,22 @@
|
||||
<artifactId>velocity-engine-core</artifactId>
|
||||
<version>2.4-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<!-- indexing -->
|
||||
<dependency>
|
||||
<groupId>org.apache.lucene</groupId>
|
||||
<artifactId>lucene-core</artifactId>
|
||||
<version>${lucene.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.lucene</groupId>
|
||||
<artifactId>lucene-analysis-common</artifactId>
|
||||
<version>${lucene.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.lucene</groupId>
|
||||
<artifactId>lucene-queryparser</artifactId>
|
||||
<version>${lucene.version}</version>
|
||||
</dependency>
|
||||
<!-- tests -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
|
@@ -21,7 +21,7 @@ abstract class OAuthHelper {
|
||||
protected get() = WebappManager.getMandatoryProperty("oauth." + name + ".secret")
|
||||
protected val redirectURI: String?
|
||||
protected get() = try {
|
||||
val uri: String = WebappManager.Companion.getProperty("webapp.url") + "/oauth.html"
|
||||
val uri: String = WebappManager.Companion.getProperty("webapp.external.url") + "/oauth.html"
|
||||
URLEncoder.encode(uri, "UTF-8")
|
||||
} catch (uee: UnsupportedEncodingException) {
|
||||
logger.error("could not encode redirect URI", uee)
|
||||
|
@@ -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,30 @@
|
||||
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.MutableObject(*pairs).also {
|
||||
it["origin"] = "EGF"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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+(?<rank>[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", "rank", "rating")
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
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()) { line ->
|
||||
val match = linePattern.matchEntire(line)
|
||||
if (match == null) {
|
||||
logger.error("could not parse line: $line")
|
||||
null
|
||||
} else {
|
||||
val pairs = groups.map {
|
||||
Pair(it, match.groups[it]?.value)
|
||||
}.toTypedArray()
|
||||
Json.MutableObject(*pairs).also {
|
||||
it["origin"] = "FFG"
|
||||
val rating = it["rating"]?.toString()?.toIntOrNull()
|
||||
if (rating != null) {
|
||||
it["rank"] = (rating/100).let { if (it < 0) "${-it}k" else "${it+1}d" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,116 @@
|
||||
package org.jeudego.pairgoth.ratings
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.apache.lucene.analysis.LowerCaseFilter
|
||||
import org.apache.lucene.analysis.standard.StandardAnalyzer
|
||||
import org.apache.lucene.document.Document
|
||||
import org.apache.lucene.document.Field
|
||||
import org.apache.lucene.document.StoredField
|
||||
import org.apache.lucene.document.StringField
|
||||
import org.apache.lucene.document.TextField
|
||||
import org.apache.lucene.index.DirectoryReader
|
||||
import org.apache.lucene.index.IndexWriter
|
||||
import org.apache.lucene.index.IndexWriterConfig
|
||||
import org.apache.lucene.index.Term
|
||||
import org.apache.lucene.queryparser.complexPhrase.ComplexPhraseQueryParser
|
||||
import org.apache.lucene.search.BooleanClause
|
||||
import org.apache.lucene.search.BooleanQuery
|
||||
import org.apache.lucene.search.FuzzyQuery
|
||||
import org.apache.lucene.search.IndexSearcher
|
||||
import org.apache.lucene.search.TermQuery
|
||||
import org.apache.lucene.store.ByteBuffersDirectory
|
||||
import org.apache.lucene.store.Directory
|
||||
import org.apache.lucene.store.NoLockFactory
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
|
||||
class PlayerIndex {
|
||||
companion object {
|
||||
val ID = "id"
|
||||
val ORIGIN = "origin"
|
||||
val NAME = "name"
|
||||
val FIRSTNAME = "firstname"
|
||||
val TEXT = "text"
|
||||
val COUNTRY = "country"
|
||||
|
||||
val MAX_HITS = 20
|
||||
val logger = LoggerFactory.getLogger("index")
|
||||
val queryParser = ComplexPhraseQueryParser(TEXT, StandardAnalyzer())
|
||||
}
|
||||
private final val directory: Directory = ByteBuffersDirectory(NoLockFactory.INSTANCE)
|
||||
private val reader by lazy { DirectoryReader.open(directory) }
|
||||
private val searcher by lazy { IndexSearcher(reader) }
|
||||
|
||||
// helper functions
|
||||
fun Json.Object.field(key: String) = getString(key) ?: throw Error("missing $key")
|
||||
fun Json.Object.nullableField(key: String) = getString(key) ?: ""
|
||||
|
||||
fun build(players: Json.Array) {
|
||||
logger.info("indexing players")
|
||||
var count = 0L
|
||||
IndexWriter(directory, IndexWriterConfig(StandardAnalyzer()).apply {
|
||||
setOpenMode(IndexWriterConfig.OpenMode.CREATE)
|
||||
}).use { writer ->
|
||||
players.forEachIndexed { i, p ->
|
||||
val player = p as Json.Object
|
||||
val origin = p.getString(ORIGIN) ?: throw Error("unknown origin")
|
||||
val text = player.field(NAME).lowercase(Locale.ROOT)
|
||||
val doc = Document()
|
||||
doc.add(StoredField(ID, i));
|
||||
doc.add(StringField(ORIGIN, player.field(ORIGIN).lowercase(Locale.ROOT), Field.Store.NO))
|
||||
doc.add(StringField(COUNTRY, player.field(COUNTRY).lowercase(Locale.ROOT), Field.Store.NO))
|
||||
doc.add(TextField(TEXT, "${player.field(NAME)} ${player.nullableField(FIRSTNAME)}", Field.Store.NO))
|
||||
writer.addDocument(doc);
|
||||
++count
|
||||
}
|
||||
}
|
||||
logger.info("indexed $count players")
|
||||
}
|
||||
|
||||
fun match(needle: String, origins: Int, country: String?): List<Int> {
|
||||
val terms = needle.lowercase(Locale.ROOT)
|
||||
.replace(Regex("([+&|!(){}\\[\\]^\\\\\"~*?:/]|(?<!\\b)-)"), "")
|
||||
.split(Regex("[ -_']+"))
|
||||
.filter { !it.isEmpty() }
|
||||
.map { "$it~" }
|
||||
.joinToString(" ")
|
||||
.let { if (it.isEmpty()) it else "$it ${it.substring(0, it.length - 1) + "*^4"}" }
|
||||
if (terms.isEmpty()) return emptyList()
|
||||
logger.info("Search query: $terms")
|
||||
val fuzzy = queryParser.parse(terms)
|
||||
val activeMask = RatingsManager.activeMask()
|
||||
val query = when (origins.countOneBits()) {
|
||||
0 -> return emptyList()
|
||||
1 -> {
|
||||
val filter = TermQuery(Term(ORIGIN, RatingsManager.Ratings.codeOf(origins)))
|
||||
BooleanQuery.Builder()
|
||||
.add(fuzzy, BooleanClause.Occur.SHOULD)
|
||||
.add(filter, BooleanClause.Occur.FILTER)
|
||||
.build()
|
||||
}
|
||||
2 -> {
|
||||
if (activeMask.countOneBits() > 2) {
|
||||
val filter =
|
||||
TermQuery(Term(ORIGIN, RatingsManager.Ratings.codeOf((origins xor activeMask) and activeMask)))
|
||||
BooleanQuery.Builder()
|
||||
.add(fuzzy, BooleanClause.Occur.SHOULD)
|
||||
.add(filter, BooleanClause.Occur.MUST_NOT)
|
||||
.build()
|
||||
} else fuzzy
|
||||
}
|
||||
3 -> fuzzy
|
||||
else -> throw Error("wrong origins mask")
|
||||
}.let {
|
||||
if (country == null) it
|
||||
else {
|
||||
val countryFilter = TermQuery(Term(COUNTRY, country.lowercase(Locale.ROOT)))
|
||||
BooleanQuery.Builder()
|
||||
.add(it, BooleanClause.Occur.SHOULD)
|
||||
.add(countryFilter, BooleanClause.Occur.FILTER)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
val docs = searcher.search(query, MAX_HITS)
|
||||
return docs.scoreDocs.map { searcher.doc(it.doc).getField(ID).numericValue().toInt() }.toList()
|
||||
}
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
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
|
||||
private var updated = false
|
||||
|
||||
val url: URL by lazy {
|
||||
WebappManager.getProperty("ratings.${origin.name.lowercase(Locale.ROOT)}")?.let { URL(it) } ?: defaultURL
|
||||
}
|
||||
|
||||
fun updateIfNeeded(): Boolean {
|
||||
return 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)
|
||||
}
|
||||
}
|
||||
true
|
||||
} else if (!this::players.isInitialized) {
|
||||
players = Json.parse(cacheFile.readText())?.asArray() ?: Json.Array()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchPlayers(): Json.Array {
|
||||
updated = 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
|
||||
fun updated() = updated
|
||||
abstract fun parsePayload(payload: String): Json.Array
|
||||
val logger = LoggerFactory.getLogger(origin.name)
|
||||
val atom = "[-._`'a-zA-ZÀ-ÿ]"
|
||||
}
|
@@ -0,0 +1,87 @@
|
||||
package org.jeudego.pairgoth.ratings
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.web.WebappManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.lang.Exception
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.ReadWriteLock
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
|
||||
object RatingsManager: Runnable {
|
||||
|
||||
enum class Ratings(val flag: Int) {
|
||||
AGA(1),
|
||||
EGF(2),
|
||||
FFG(4);
|
||||
companion object {
|
||||
fun valueOf(mask: Int): Ratings {
|
||||
if (mask.countOneBits() != 1) throw Error("wrong use")
|
||||
return values().filter { it.flag == mask }.firstOrNull() ?: throw Error("wrong mask")
|
||||
}
|
||||
fun codeOf(mask: Int) = valueOf(mask).name.lowercase(Locale.ROOT)
|
||||
}
|
||||
}
|
||||
|
||||
val ratingsHandlers by lazy {
|
||||
mapOf(
|
||||
Pair(Ratings.AGA, AGARatingsHandler),
|
||||
Pair(Ratings.EGF, EGFRatingsHandler),
|
||||
Pair(Ratings.FFG, FFGRatingsHandler)
|
||||
);
|
||||
}
|
||||
|
||||
fun activeMask() = ratingsHandlers.entries.filter { it.value.active }.map { it.key.flag }.reduce { a,b -> a or b }
|
||||
|
||||
val timer = Timer()
|
||||
lateinit var players: Json.MutableArray
|
||||
val updateLock: ReadWriteLock = ReentrantReadWriteLock()
|
||||
override fun run() {
|
||||
logger.info("launching ratings manager")
|
||||
timer.scheduleAtFixedRate(Task, 0L, 3600000L)
|
||||
}
|
||||
object Task: TimerTask() {
|
||||
override fun run() {
|
||||
try {
|
||||
players = ratingsHandlers.values.filter { it.active }.flatMapTo(Json.MutableArray()) { ratings ->
|
||||
ratings.fetchPlayers()
|
||||
}
|
||||
val updated = ratingsHandlers.values.filter { it.active }.map { it.updated() }.reduce { u1, u2 ->
|
||||
u1 or u2
|
||||
}
|
||||
if (updated) {
|
||||
try {
|
||||
updateLock.writeLock().lock()
|
||||
index.build(players)
|
||||
} finally {
|
||||
updateLock.writeLock().unlock()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("could not build or refresh index", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
fun search(needle: String, aga: Boolean, egf: Boolean, ffg: Boolean, country: String?): Json.Array {
|
||||
try {
|
||||
updateLock.readLock().lock()
|
||||
var mask = 0
|
||||
if (aga && ratingsHandlers[Ratings.AGA]!!.active) mask = mask or Ratings.AGA.flag
|
||||
if (egf && ratingsHandlers[Ratings.EGF]!!.active) mask = mask or Ratings.EGF.flag
|
||||
if (ffg && ratingsHandlers[Ratings.FFG]!!.active) mask = mask or Ratings.FFG.flag
|
||||
val matches = index.match(needle, mask, country)
|
||||
return matches.map { it -> players[it] }.toCollection(Json.MutableArray())
|
||||
} finally {
|
||||
updateLock.readLock().unlock()
|
||||
}
|
||||
|
||||
}
|
||||
val index = PlayerIndex()
|
||||
}
|
@@ -80,11 +80,15 @@ class Translator private constructor(private val iso: String) {
|
||||
if (groupStart == -1) throw RuntimeException("unexpected case")
|
||||
if (groupStart > start) output.print(text.substring(start, groupStart))
|
||||
val capture = matcher.group(group)
|
||||
var token: String = StringEscapeUtils.unescapeHtml4(capture)
|
||||
|
||||
// CB TODO - unescape and escape steps removed, because it breaks text blocks containing unescaped quotes.
|
||||
// See how it impacts the remaining.
|
||||
|
||||
var token: String = capture // StringEscapeUtils.unescapeHtml4(capture)
|
||||
if (StringUtils.containsOnly(token, "\r\n\t -;:.'\"/<>\u00A00123456789€[]!")) output.print(capture) else {
|
||||
token = normalize(token)
|
||||
token = translate(token)
|
||||
output.print(StringEscapeUtils.escapeHtml4(token))
|
||||
output.print(token) // (StringEscapeUtils.escapeHtml4(token))
|
||||
}
|
||||
val groupEnd = matcher.end(group)
|
||||
if (groupEnd < end) output.print(text.substring(groupEnd, end))
|
||||
@@ -132,7 +136,7 @@ class Translator private constructor(private val iso: String) {
|
||||
get() = textAccessor[this] as String
|
||||
set(value: String) { textAccessor[this] = value }
|
||||
|
||||
private val saveMissingTranslations = System.getProperty("pairgoth.env") == "dev"
|
||||
private val saveMissingTranslations = System.getProperty("pairgoth.webapp.env") == "dev"
|
||||
private val missingTranslations: MutableSet<String> = ConcurrentSkipListSet()
|
||||
|
||||
private fun reportMissingTranslation(enText: String) {
|
||||
@@ -145,7 +149,7 @@ class Translator private constructor(private val iso: String) {
|
||||
private val logger = LoggerFactory.getLogger("translation")
|
||||
private val translatedTemplates: MutableMap<Pair<String, String>, Template> = ConcurrentHashMap<Pair<String, String>, Template>()
|
||||
private val textExtractor = Pattern.compile(
|
||||
"<[^>]+\\splaceholder=\"(?<placeholder>[^\"]*)\"[^>]*>|(?<=>)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?<text>[^<>]+?)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?=<|$)|(?<=>|^)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?<text2>[^<>]+?)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?=<)|^(?:[ \\r\\n\\t /–-]| |‐)*(?<text3>[^<>]+?)(?:[ \\r\\n\\t /–-]| |‐)*(?=$)",
|
||||
"<[^>]+\\s(?:placeholder|title)=\"(?<placeholder>[^\"]*)\"[^>]*>|(?<=>)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?<text>[^<>]+?)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?=<|$)|(?<=>|^)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?<text2>[^<>]+?)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?=<)|^(?:[ \\r\\n\\t /–-]| |‐)*(?<text3>[^<>]+?)(?:[ \\r\\n\\t /–-]| |‐)*(?=$)",
|
||||
Pattern.DOTALL
|
||||
)
|
||||
private val ignoredTags = setOf("head", "script", "style")
|
||||
@@ -168,6 +172,7 @@ class Translator private constructor(private val iso: String) {
|
||||
|
||||
val providedLanguages = setOf("en", "fr")
|
||||
const val defaultLanguage = "en"
|
||||
const val defaultLocale = "en"
|
||||
|
||||
internal fun notifyExiting() {
|
||||
translators.values.filter {
|
||||
|
@@ -6,27 +6,37 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.internal.EMPTY_REQUEST
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class ApiTool {
|
||||
companion object {
|
||||
const val JSON = "application/json"
|
||||
val apiRoot =
|
||||
System.getProperty("pairgoth.api.url")?.let { "${it.removeSuffix("/")}/" }
|
||||
?: System.getProperty("pairgoth.webapp.url")?.let { "${it.removeSuffix("/")}/api/" }
|
||||
val apiRoot = System.getProperty("pairgoth.api.external.url")?.let { "${it.removeSuffix("/")}/" }
|
||||
?: throw Error("no configured API url")
|
||||
val logger = LoggerFactory.getLogger("api")
|
||||
}
|
||||
private val client = OkHttpClient()
|
||||
private fun prepare(url: String) = Request.Builder().url("$apiRoot$url").header("Accept", JSON)
|
||||
private fun Json.toRequestBody() = toString().toRequestBody(JSON.toMediaType())
|
||||
private fun Request.Builder.process(): Json {
|
||||
client.newCall(build()).execute().use { response ->
|
||||
if (response.isSuccessful) {
|
||||
when (response.body?.contentType()?.subtype) {
|
||||
null -> throw Error("null body or content type")
|
||||
"json" -> return Json.parse(response.body!!.string()) ?: throw Error("could not parse json")
|
||||
else -> throw Error("unhandled content type: ${response.body!!.contentType()}")
|
||||
try {
|
||||
return client.newCall(build()).execute().use { response ->
|
||||
if (response.isSuccessful) {
|
||||
when (response.body?.contentType()?.subtype) {
|
||||
null -> throw Error("null body or content type")
|
||||
"json" -> Json.parse(response.body!!.string()) ?: throw Error("could not parse json")
|
||||
else -> throw Error("unhandled content type: ${response.body!!.contentType()}")
|
||||
}
|
||||
} else {
|
||||
when (response.body?.contentType()?.subtype) {
|
||||
"json" -> Json.parse(response.body!!.string()) ?: throw Error("could not parse error json")
|
||||
else -> throw Error("${response.code} ${response.message}")
|
||||
}
|
||||
}
|
||||
} else throw Error("api call failed: ${response.code} ${response.message}")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logger.error("api call failed", e)
|
||||
return Json.Object("error" to e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,239 @@
|
||||
package org.jeudego.pairgoth.view
|
||||
|
||||
import org.apache.velocity.tools.config.ValidScope
|
||||
import org.jeudego.pairgoth.web.WebappManager
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
@ValidScope("request")
|
||||
class CountriesTool {
|
||||
|
||||
public var country: Pair<String, String>? = null
|
||||
|
||||
public fun configure(params: Map<*, *>) {
|
||||
val request = params["request"]!! as HttpServletRequest
|
||||
country = request.getHeader("Accept-Language")?.let { header ->
|
||||
langHeaderParser.find(header)
|
||||
}?.let { match ->
|
||||
match.groupValues.getOrNull(2)?.lowercase() ?: match.groupValues[1].lowercase()
|
||||
}?.let { iso ->
|
||||
countries[iso]?.let { name ->
|
||||
Pair(iso, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun getCountries() = countries.entries
|
||||
|
||||
companion object {
|
||||
private val langHeaderParser = Regex("(?:\\b(\\*|[a-z]{2})(?:(?:_|-)([a-z]{2}))?)(?:;q=([0-9.]+))?", RegexOption.IGNORE_CASE)
|
||||
public val countries = mapOf(
|
||||
"ad" to "Andorra",
|
||||
"ae" to "United Arab Emirates",
|
||||
"af" to "Afghanistan",
|
||||
"ai" to "Anguilla",
|
||||
"al" to "Albania",
|
||||
"am" to "Armenia",
|
||||
"ao" to "Angola",
|
||||
"ar" to "Argentina",
|
||||
"at" to "Austria",
|
||||
"au" to "Australia",
|
||||
"aw" to "Aruba",
|
||||
"az" to "Azerbaijan",
|
||||
"ba" to "Bosnia and Herzegovina",
|
||||
"bb" to "Barbados",
|
||||
"bd" to "Bangladesh",
|
||||
"be" to "Belgium",
|
||||
"bf" to "Burkina Faso",
|
||||
"bg" to "Bulgaria",
|
||||
"bh" to "Bahrain",
|
||||
"bi" to "Burundi",
|
||||
"bj" to "Benin",
|
||||
"bl" to "Saint Barthélemy",
|
||||
"bm" to "Bermuda",
|
||||
"bo" to "Bolivia",
|
||||
"br" to "Brazil",
|
||||
"bs" to "Bahamas",
|
||||
"bt" to "Bhutan",
|
||||
"bw" to "Botswana",
|
||||
"by" to "Belarus",
|
||||
"bz" to "Belize",
|
||||
"ca" to "Canada",
|
||||
"cd" to "Dem. Rep. of the Congo",
|
||||
"cf" to "Central African Republic",
|
||||
"cg" to "Congo",
|
||||
"ch" to "Switzerland",
|
||||
"ci" to "Ivory Coast",
|
||||
"cl" to "Chile",
|
||||
"cm" to "Cameroon",
|
||||
"cn" to "China",
|
||||
"co" to "Colombia",
|
||||
"cr" to "Costa Rica",
|
||||
"cu" to "Cuba",
|
||||
"cv" to "Cabo Verde",
|
||||
"cw" to "Curaçao",
|
||||
"cy" to "Cyprus",
|
||||
"cz" to "Czechia",
|
||||
"de" to "Germany",
|
||||
"dj" to "Djibouti",
|
||||
"dk" to "Denmark",
|
||||
"dm" to "Dominica",
|
||||
"do" to "Dominican Republic",
|
||||
"dz" to "Algeria",
|
||||
"ec" to "Ecuador",
|
||||
"ee" to "Estonia",
|
||||
"eg" to "Egypt",
|
||||
"eh" to "Western Sahara*",
|
||||
"er" to "Eritrea",
|
||||
"es" to "Spain",
|
||||
"et" to "Ethiopia",
|
||||
"fi" to "Finland",
|
||||
"fj" to "Fiji",
|
||||
"fr" to "France",
|
||||
"ga" to "Gabon",
|
||||
"gb" to "United Kingdom",
|
||||
"gd" to "Grenada",
|
||||
"ge" to "Georgia",
|
||||
"gg" to "Guernsey",
|
||||
"gh" to "Ghana",
|
||||
"gi" to "Gibraltar",
|
||||
"gl" to "Greenland",
|
||||
"gm" to "Gambia",
|
||||
"gn" to "Guinea",
|
||||
"gq" to "Equatorial Guinea",
|
||||
"gr" to "Greece",
|
||||
"gt" to "Guatemala",
|
||||
"gu" to "Guam",
|
||||
"gw" to "Guinea-Bissau",
|
||||
"gy" to "Guyana",
|
||||
"hk" to "Hong Kong",
|
||||
"hn" to "Honduras",
|
||||
"hr" to "Croatia",
|
||||
"ht" to "Haiti",
|
||||
"hu" to "Hungary",
|
||||
"id" to "Indonesia",
|
||||
"ie" to "Ireland",
|
||||
"il" to "Israel",
|
||||
"im" to "Isle of Man",
|
||||
"in" to "India",
|
||||
"iq" to "Iraq",
|
||||
"ir" to "Iran",
|
||||
"is" to "Iceland",
|
||||
"it" to "Italy",
|
||||
"je" to "Jersey",
|
||||
"jm" to "Jamaica",
|
||||
"jo" to "Jordan",
|
||||
"jp" to "Japan",
|
||||
"ke" to "Kenya",
|
||||
"kg" to "Kyrgyzstan",
|
||||
"kh" to "Cambodia",
|
||||
"ki" to "Kiribati",
|
||||
"km" to "Comoros",
|
||||
"kp" to "North Korea",
|
||||
"kr" to "South Korea",
|
||||
"kw" to "Kuwait",
|
||||
"kz" to "Kazakhstan",
|
||||
"la" to "Lao People's Dem. Rep.",
|
||||
"lb" to "Lebanon",
|
||||
"li" to "Liechtenstein",
|
||||
"lk" to "Sri Lanka",
|
||||
"lr" to "Liberia",
|
||||
"ls" to "Lesotho",
|
||||
"lt" to "Lithuania",
|
||||
"lu" to "Luxembourg",
|
||||
"lv" to "Latvia",
|
||||
"ly" to "Libya",
|
||||
"ma" to "Morocco",
|
||||
"mc" to "Monaco",
|
||||
"md" to "Moldova",
|
||||
"me" to "Montenegro",
|
||||
"mg" to "Madagascar",
|
||||
"mk" to "Macedonia",
|
||||
"ml" to "Mali",
|
||||
"mm" to "Myanmar",
|
||||
"mn" to "Mongolia",
|
||||
"mo" to "Macao",
|
||||
"mq" to "Martinique",
|
||||
"mr" to "Mauritania",
|
||||
"ms" to "Montserrat",
|
||||
"mt" to "Malta",
|
||||
"mu" to "Mauritius",
|
||||
"mv" to "Maldives",
|
||||
"mw" to "Malawi",
|
||||
"mx" to "Mexico",
|
||||
"my" to "Malaysia",
|
||||
"mz" to "Mozambique",
|
||||
"na" to "Namibia",
|
||||
"nc" to "New Caledonia",
|
||||
"ne" to "Niger",
|
||||
"ng" to "Nigeria",
|
||||
"ni" to "Nicaragua",
|
||||
"nl" to "The Netherlands",
|
||||
"no" to "Norway",
|
||||
"np" to "Nepal",
|
||||
"nr" to "Nauru",
|
||||
"nu" to "Niue",
|
||||
"nz" to "New Zealand",
|
||||
"om" to "Oman",
|
||||
"pa" to "Panama",
|
||||
"pe" to "Peru",
|
||||
"pf" to "French Polynesia",
|
||||
"pg" to "Papua New Guinea",
|
||||
"ph" to "The Philippines",
|
||||
"pk" to "Pakistan",
|
||||
"pl" to "Poland",
|
||||
"pn" to "Pitcairn",
|
||||
"pr" to "Puerto Rico",
|
||||
"pt" to "Portugal",
|
||||
"pw" to "Palau",
|
||||
"py" to "Paraguay",
|
||||
"qa" to "Qatar",
|
||||
"re" to "Réunion",
|
||||
"ro" to "Romania",
|
||||
"rs" to "Serbia",
|
||||
"ru" to "Russia",
|
||||
"rw" to "Rwanda",
|
||||
"sa" to "Saudi Arabia",
|
||||
"sc" to "Seychelles",
|
||||
"sd" to "Sudan",
|
||||
"se" to "Sweden",
|
||||
"sg" to "Singapore",
|
||||
"si" to "Slovenia",
|
||||
"sk" to "Slovakia",
|
||||
"sl" to "Sierra Leone",
|
||||
"sm" to "San Marino",
|
||||
"sn" to "Senegal",
|
||||
"so" to "Somalia",
|
||||
"sr" to "Suriname",
|
||||
"ss" to "South Sudan",
|
||||
"sv" to "El Salvador",
|
||||
"sy" to "Syrian Arab Republic",
|
||||
"sz" to "Swaziland",
|
||||
"td" to "Chad",
|
||||
"tg" to "Togo",
|
||||
"th" to "Thailand",
|
||||
"tj" to "Tajikistan",
|
||||
"tm" to "Turkmenistan",
|
||||
"tn" to "Tunisia",
|
||||
"to" to "Tonga",
|
||||
"tr" to "Turkey",
|
||||
"tv" to "Tuvalu",
|
||||
"tw" to "Taiwan",
|
||||
"tz" to "Tanzania",
|
||||
"ua" to "Ukraine",
|
||||
"ug" to "Uganda",
|
||||
"us" to "United States of America",
|
||||
"uy" to "Uruguay",
|
||||
"uz" to "Uzbekistan",
|
||||
"ve" to "Venezuela",
|
||||
"vn" to "Viet Nam",
|
||||
"vu" to "Vanuatu",
|
||||
"ws" to "Samoa",
|
||||
"xk" to "Kosovo",
|
||||
"ye" to "Yemen",
|
||||
"yt" to "Mayotte",
|
||||
"za" to "South Africa",
|
||||
"zm" to "Zambia",
|
||||
"zw" to "Zimbabwe"
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package org.jeudego.pairgoth.view
|
||||
|
||||
import com.republicate.kson.Json
|
||||
|
||||
/**
|
||||
* Generic utilities
|
||||
*/
|
||||
|
||||
class PairgothTool {
|
||||
public fun toMap(array: Json.Array) = array.map { ser -> ser as Json.Object }.associateBy { it.getLong("id")!! }
|
||||
}
|
@@ -21,6 +21,14 @@ class TranslationTool {
|
||||
}.let { Pair(iso, it) }
|
||||
}
|
||||
|
||||
val defaultCountry = Translator.providedLanguages.associate { iso ->
|
||||
when (iso) {
|
||||
"en" -> "gb"
|
||||
"zh" -> "cn"
|
||||
else -> iso
|
||||
}.let { Pair(iso, it) }
|
||||
}
|
||||
|
||||
fun url(request: HttpServletRequest, lang: String): String {
|
||||
val out = StringBuilder()
|
||||
out.append(request.requestURL.replaceFirst(Regex("://"), "://$lang/"))
|
||||
@@ -29,7 +37,12 @@ class TranslationTool {
|
||||
return out.toString()
|
||||
}
|
||||
|
||||
fun datepickerLocale(language: String, locale: String) =
|
||||
if (datepickerLocales.contains(locale)) locale
|
||||
else language
|
||||
|
||||
companion object {
|
||||
val datepickerLocales = setOf("ar-DZ", "ar", "ar-TN", "az", "bg", "bm", "bn", "br", "bs", "ca", "cs", "cy", "da", "de", "el", "en-AU", "en-CA", "en-GB", "en-IE", "en-NZ", "en-ZA", "eo", "es", "et", "eu", "fa", "fi", "fo", "fr-CH", "fr", "gl", "he", "hi", "hr", "hu", "hy", "id", "is", "it-CH", "it", "ja", "ka", "kk", "km", "ko", "lt", "lv", "me", "mk", "mn", "mr", "ms", "nl-BE", "nl", "no", "oc", "pl", "pt-BR", "pt", "ro", "ru", "si", "sk", "sl", "sq", "sr", "sr-latn", "sv", "sw", "ta", "tg", "th", "tk", "tr", "uk", "uz-cyrl", "uz-latn", "vi", "zh-CN", "zh-TW")
|
||||
val translator = ThreadLocal<Translator>()
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package org.jeudego.pairgoth.web
|
||||
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.proxy.AsyncProxyServlet;
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
class ApiServlet : AsyncProxyServlet() {
|
||||
|
||||
override fun addProxyHeaders(clientRequest: HttpServletRequest, proxyRequest: Request) {
|
||||
// proxyRequest.header("X-EGC-User", some user id...)
|
||||
}
|
||||
|
||||
override fun rewriteTarget(clientRequest: HttpServletRequest): String {
|
||||
val uri = clientRequest.requestURI
|
||||
if (!uri.startsWith("/api/")) throw Error("unhandled API uri: $uri")
|
||||
val path = uri.substringAfter("/api")
|
||||
val qr = clientRequest.queryString?.let { "?$it" } ?: ""
|
||||
return "$apiRoot$path$qr"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val apiRoot = System.getProperty("pairgoth.api.external.url")?.let { "${it.removeSuffix("/")}" }
|
||||
?: throw Error("no configured API url")
|
||||
}
|
||||
}
|
@@ -32,7 +32,7 @@ class DispatchingFilter : Filter {
|
||||
when {
|
||||
uri.endsWith('/') -> response.sendRedirect("${uri}index")
|
||||
uri.contains('.') ->
|
||||
if (uri.endsWith(".html")) resp.sendError(404)
|
||||
if (uri.endsWith(".html") || uri.contains(".inc.")) resp.sendError(404)
|
||||
else defaultRequestDispatcher.forward(request, response)
|
||||
else -> chain.doFilter(request, response)
|
||||
}
|
||||
|
@@ -2,8 +2,10 @@ package org.jeudego.pairgoth.web
|
||||
|
||||
import org.jeudego.pairgoth.util.Translator
|
||||
import org.jeudego.pairgoth.util.Translator.Companion.defaultLanguage
|
||||
import org.jeudego.pairgoth.util.Translator.Companion.defaultLocale
|
||||
import org.jeudego.pairgoth.util.Translator.Companion.providedLanguages
|
||||
import org.jeudego.pairgoth.view.TranslationTool
|
||||
import java.util.*
|
||||
import javax.servlet.Filter
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.FilterConfig
|
||||
@@ -22,48 +24,63 @@ class LanguageFilter : Filter {
|
||||
override fun doFilter(req: ServletRequest, resp: ServletResponse, chain: FilterChain) {
|
||||
val request = req as HttpServletRequest
|
||||
val response = resp as HttpServletResponse
|
||||
val uri = request.requestURI
|
||||
|
||||
if (uri.startsWith("/api/")) {
|
||||
chain.doFilter(request, response)
|
||||
return
|
||||
}
|
||||
|
||||
val match = langPattern.matchEntire(uri)
|
||||
val askedLanguage = match?.groupValues?.get(1)
|
||||
val target = match?.groupValues?.get(2) ?: uri
|
||||
|
||||
val reqLang = request.getAttribute("lang") as String?
|
||||
if (reqLang != null) {
|
||||
// this is a forwarded request, language and locale should already have been set
|
||||
TranslationTool.translator.set(Translator.getTranslator(reqLang))
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
val uri = request.requestURI
|
||||
val match = langPattern.matchEntire(uri)
|
||||
val lang = match?.groupValues?.get(1)
|
||||
val target = match?.groupValues?.get(2) ?: uri
|
||||
|
||||
if (lang != null && providedLanguages.contains(lang)) {
|
||||
val (preferredLanguage, preferredLocale) = parseLanguageHeader(request)
|
||||
if (askedLanguage != null && providedLanguages.contains(askedLanguage)) {
|
||||
// the target URI contains a language we provide
|
||||
request.setAttribute("lang", lang)
|
||||
request.setAttribute("lang", askedLanguage)
|
||||
request.setAttribute("loc",
|
||||
if (askedLanguage == preferredLanguage) preferredLocale
|
||||
else askedLanguage
|
||||
)
|
||||
request.setAttribute("target", target)
|
||||
filterConfig!!.servletContext.getRequestDispatcher(target).forward(request, response)
|
||||
} else {
|
||||
// the request must be redirected
|
||||
val preferredLanguage = getPreferredLanguage(request)
|
||||
val destination = if (lang != null) target else uri
|
||||
val destination = if (askedLanguage != null) target else uri
|
||||
response.sendRedirect("/${preferredLanguage}${destination}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPreferredLanguage(request: HttpServletRequest): String {
|
||||
return (request.session.getAttribute("lang") as String?) ?:
|
||||
( langHeaderParser.findAll(request.getHeader("Accept-Language") ?: "").filter {
|
||||
/**
|
||||
* Returns Pair(language, locale)
|
||||
*/
|
||||
private fun parseLanguageHeader(request: HttpServletRequest): Pair<String, String> {
|
||||
langHeaderParser.findAll(request.getHeader("Accept-Language") ?: "").filter {
|
||||
providedLanguages.contains(it.groupValues[1])
|
||||
}.sortedByDescending {
|
||||
it.groupValues[2].toDoubleOrNull() ?: 1.0
|
||||
}.firstOrNull()?.let {
|
||||
it.groupValues[1]
|
||||
} ?: defaultLanguage ).also {
|
||||
request.session.setAttribute("lang", it)
|
||||
it.groupValues[3].toDoubleOrNull() ?: 1.0
|
||||
}.firstOrNull()?.let { match ->
|
||||
val lang = match.groupValues[1].let { if (it == "*") defaultLanguage else it }
|
||||
val variant = match.groupValues.getOrNull(2)?.lowercase(Locale.ROOT)
|
||||
// by convention, the variant is only kept if different from the language (fr-FR => fr)
|
||||
val locale = variant?.let { if (lang == variant) lang else "$lang-${variant.uppercase(Locale.ROOT)}" } ?: lang
|
||||
return Pair(lang, locale)
|
||||
}
|
||||
return Pair(defaultLanguage, defaultLanguage)
|
||||
}
|
||||
|
||||
override fun destroy() {}
|
||||
|
||||
companion object {
|
||||
private val langPattern = Regex("/([a-z]{2})(/.+)")
|
||||
private val langHeaderParser = Regex("(?:\\b(\\*|[a-z]{2})(?:_\\w+)?)(?:;q=([0-9.]+))?")
|
||||
private val langHeaderParser = Regex("(?:\\b(\\*|[a-z]{2})(?:(?:_|-)(\\w+))?)(?:;q=([0-9.]+))?")
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,159 @@
|
||||
package org.jeudego.pairgoth.web
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.ratings.RatingsManager
|
||||
import org.jeudego.pairgoth.util.Colorizer
|
||||
import org.jeudego.pairgoth.util.parse
|
||||
import org.jeudego.pairgoth.util.toString
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import javax.servlet.http.HttpServlet
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
class SearchServlet: HttpServlet() {
|
||||
|
||||
public override fun doPost(request: HttpServletRequest, response: HttpServletResponse) {
|
||||
val uri = request.requestURI
|
||||
logger.logRequest(request, !uri.contains(".") && uri.length > 1)
|
||||
var payload: Json? = null
|
||||
var reason = "OK"
|
||||
try {
|
||||
validateContentType(request)
|
||||
val query = request.getAttribute(PAYLOAD_KEY) as Json.Object? ?: throw ApiException(HttpServletResponse.SC_BAD_REQUEST, "no payload")
|
||||
val needle = query.getString("needle") ?: throw ApiException(HttpServletResponse.SC_BAD_REQUEST, "no needle")
|
||||
val country = query.getString("countryFilter")
|
||||
val aga = query.getBoolean("aga") ?: false
|
||||
val egf = query.getBoolean("egf") ?: false
|
||||
val ffg = query.getBoolean("ffg") ?: false
|
||||
payload = RatingsManager.search(needle, aga, egf, ffg, country)
|
||||
setContentType(response)
|
||||
payload.toString(response.writer)
|
||||
} catch (ioe: IOException) {
|
||||
logger.error(Colorizer.red("could not process call"), ioe)
|
||||
reason = ioe.message ?: "unknown i/o exception"
|
||||
error(request, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, reason, ioe)
|
||||
} finally {
|
||||
val builder = StringBuilder()
|
||||
builder.append(response.status).append(' ')
|
||||
.append(reason)
|
||||
if (response.status == HttpServletResponse.SC_INTERNAL_SERVER_ERROR) {
|
||||
logger.trace(Colorizer.red(">> {}"), builder.toString())
|
||||
} else {
|
||||
logger.trace(Colorizer.green(">> {}"), builder.toString())
|
||||
}
|
||||
|
||||
// CB TODO - should be bufferized and asynchronously written in synchronous chunks
|
||||
// so that header lines from parallel requests are not mixed up in the logs ;
|
||||
// synchronizing the whole request log is not desirable
|
||||
for (header in response.headerNames) {
|
||||
val value = response.getHeader(header)
|
||||
logger.trace(Colorizer.green(">> {}: {}"), header, value)
|
||||
}
|
||||
if (payload != null) {
|
||||
try {
|
||||
logger.logPayload(">> ", payload, false)
|
||||
} catch (ioe: IOException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Throws(ApiException::class)
|
||||
protected fun validateContentType(request: HttpServletRequest) {
|
||||
// extract content type parts
|
||||
val contentType = request.contentType
|
||||
if (contentType == null) {
|
||||
if (request.method == "GET") return
|
||||
throw ApiException(
|
||||
HttpServletResponse.SC_BAD_REQUEST,
|
||||
"no content type header"
|
||||
)
|
||||
}
|
||||
val sep = contentType.indexOf(';')
|
||||
val mimeType: String
|
||||
var charset: String? = null
|
||||
if (sep == -1) mimeType = contentType else {
|
||||
mimeType = contentType.substring(0, sep).trim { it <= ' ' }
|
||||
val params =
|
||||
contentType.substring(sep + 1).split("=".toRegex()).dropLastWhile { it.isEmpty() }
|
||||
.toTypedArray()
|
||||
if (params.size == 2 && params[0].lowercase(Locale.getDefault())
|
||||
.trim { it <= ' ' } == "charset"
|
||||
) {
|
||||
charset = params[1].lowercase(Locale.getDefault()).trim { it <= ' ' }
|
||||
.replace("-".toRegex(), "")
|
||||
}
|
||||
}
|
||||
|
||||
// check charset
|
||||
if (charset != null && EXPECTED_CHARSET != charset.lowercase(Locale.ROOT).replace("-", "")) throw ApiException(
|
||||
HttpServletResponse.SC_BAD_REQUEST,
|
||||
"UTF-8 content expected"
|
||||
)
|
||||
|
||||
// check content type
|
||||
if (isJson(mimeType)) {
|
||||
// put Json body as request attribute
|
||||
try {
|
||||
Json.parse(request.reader)?.let { payload: Json ->
|
||||
request.setAttribute(PAYLOAD_KEY, payload)
|
||||
if (logger.isInfoEnabled) {
|
||||
logger.logPayload("<< ", payload, true)
|
||||
}
|
||||
}
|
||||
} catch (ioe: IOException) {
|
||||
throw ApiException(HttpServletResponse.SC_BAD_REQUEST, ioe)
|
||||
}
|
||||
}
|
||||
else throw ApiException(
|
||||
HttpServletResponse.SC_BAD_REQUEST,
|
||||
"JSON content expected"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
protected fun error(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
code: Int,
|
||||
message: String?,
|
||||
cause: Throwable? = null
|
||||
) {
|
||||
try {
|
||||
if (code == 500 || response.isCommitted) {
|
||||
logger.error(
|
||||
"Request {} {} gave error {} {}",
|
||||
request.method,
|
||||
request.requestURI,
|
||||
code,
|
||||
message,
|
||||
cause
|
||||
)
|
||||
}
|
||||
response.status = code
|
||||
if (response.isCommitted) return
|
||||
val errorPayload = Json.Object(
|
||||
"success" to false,
|
||||
"error" to (message ?: "unknown error")
|
||||
)
|
||||
setContentType(response)
|
||||
errorPayload.toString(response.writer)
|
||||
} catch (ioe: IOException) {
|
||||
logger.error("Could not send back error", ioe)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun setContentType(response: HttpServletResponse) {
|
||||
response.contentType = "application/json; charset=UTF-8"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var logger = LoggerFactory.getLogger("search")
|
||||
private const val EXPECTED_CHARSET = "utf8"
|
||||
const val PAYLOAD_KEY = "PAYLOAD"
|
||||
fun isJson(mimeType: String) = "text/json" == mimeType || "application/json" == mimeType || mimeType.endsWith("+json")
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ import org.apache.velocity.context.Context
|
||||
import org.apache.velocity.exception.ResourceNotFoundException
|
||||
import org.apache.velocity.tools.view.ServletUtils
|
||||
import org.apache.velocity.tools.view.VelocityViewServlet
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.URLDecoder
|
||||
@@ -95,7 +96,14 @@ class ViewServlet : VelocityViewServlet() {
|
||||
|
||||
}
|
||||
|
||||
override fun doRequest(request: HttpServletRequest, response: HttpServletResponse ) {
|
||||
// val uri = request.requestURI
|
||||
logger.logRequest(request) //, !uri.contains(".") && uri.length > 1)
|
||||
super.doRequest(request, response)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var logger = LoggerFactory.getLogger("view")
|
||||
private const val STANDARD_LAYOUT = "/WEB-INF/layouts/standard.html"
|
||||
}
|
||||
}
|
@@ -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,7 +51,9 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
|
||||
override fun contextInitialized(sce: ServletContextEvent) {
|
||||
context = sce.servletContext
|
||||
logger.info("---------- Starting $WEBAPP_NAME ----------")
|
||||
context.setAttribute("manager", this)
|
||||
logger.info("info level is active")
|
||||
logger.debug("debug level is active")
|
||||
logger.trace("trace level is active")
|
||||
webappRoot = context.getRealPath("/")
|
||||
try {
|
||||
// load default properties
|
||||
@@ -64,7 +64,11 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
|
||||
properties[(key as String).removePrefix(PAIRGOTH_PROPERTIES_PREFIX)] = value
|
||||
}
|
||||
|
||||
logger.info("Using profile {}", properties.getProperty("webapp.env"))
|
||||
val env = properties.getProperty("env")
|
||||
logger.info("Using profile {}", )
|
||||
|
||||
// let the view be aware of the environment
|
||||
context.setAttribute("env", env)
|
||||
|
||||
// set system user agent string to empty string
|
||||
System.setProperty("http.agent", "")
|
||||
@@ -73,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)
|
||||
}
|
||||
@@ -111,10 +118,10 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
|
||||
return properties.getProperty(prop)
|
||||
}
|
||||
fun getMandatoryProperty(prop: String): String {
|
||||
return properties.getProperty(prop) ?: throw Error("missing property: ${prop}")
|
||||
return getProperty(prop) ?: throw Error("missing property: ${prop}")
|
||||
}
|
||||
|
||||
val webappURL by lazy { getProperty("webapp.url") }
|
||||
val webappURL by lazy { getProperty("webapp.external.url") }
|
||||
|
||||
private val services = mutableMapOf<String, Pair<Runnable, Thread>>()
|
||||
|
||||
|
@@ -1,7 +1,8 @@
|
||||
@import "/fonts/hornbill.css";
|
||||
@import "/lib/fomantic-ui-2.9.2/semantic.min.css" layer(semantic);
|
||||
|
||||
@layer pairgoth {
|
||||
|
||||
|
||||
/* general styles */
|
||||
body {
|
||||
font-size: clamp(14px, 1rem + 1vw, 24px);
|
||||
@@ -24,6 +25,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: Hornbill, serif;
|
||||
font-weight: bolder;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nobreak {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* header, center, footer */
|
||||
|
||||
#header {
|
||||
@@ -42,6 +57,7 @@
|
||||
}
|
||||
|
||||
#center {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
#inner {
|
||||
@@ -63,6 +79,7 @@
|
||||
}
|
||||
|
||||
.section {
|
||||
text-align: center;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
@@ -77,7 +94,7 @@
|
||||
background-color: black;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
z-index: 1000;
|
||||
z-index: 50;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -136,27 +153,31 @@
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transform: scale(1.2);
|
||||
#lang-list {
|
||||
position: absolute;
|
||||
display: none;
|
||||
top:100%;
|
||||
right: 1em;
|
||||
flex-flow: column nowrap;
|
||||
padding: 0.5em;
|
||||
gap: 0.5em;
|
||||
background-color: #dddddd;
|
||||
align-items: flex-start;
|
||||
z-index: 50;
|
||||
&.shown {
|
||||
display: flex;
|
||||
}
|
||||
a {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
i {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
#lang-list {
|
||||
position: fixed;
|
||||
display: none;
|
||||
top:3em;
|
||||
right: 1em;
|
||||
flex-flow: column nowrap;
|
||||
padding: 0.5em;
|
||||
gap: 0.5em;
|
||||
background-color: #dddddd;
|
||||
align-items: flex-start;
|
||||
z-index: 60;
|
||||
&.shown {
|
||||
display: flex;
|
||||
}
|
||||
.lang {
|
||||
cursor: pointer;
|
||||
}
|
||||
a {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
i {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,6 +185,7 @@
|
||||
/* UI fixes */
|
||||
|
||||
.ui.form, .ui.segment, .ui.form .field > label { font-size: 1em; }
|
||||
.ui.form .fields { }
|
||||
span > input[type="radio"] { vertical-align: text-top; }
|
||||
span > input[type="text"] { vertical-align: baseline; width: initial; }
|
||||
span > input.date { vertical-align: baseline; width: 8em; }
|
||||
@@ -175,10 +197,150 @@
|
||||
.step:last-child { padding-right: 1em; }
|
||||
.step .description { display: none; }
|
||||
|
||||
.ui.form input[type=text], .ui.form input[type="number"], .ui.form select {
|
||||
padding: 0.4em 0.2em;
|
||||
}
|
||||
.ui.form input[type="number"], input.duration {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
position: sticky;
|
||||
bottom: 1em;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
padding: 0.2em 0.1em 0.2em 1em;
|
||||
vertical-align: baseline;
|
||||
width: 3.5em;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.roundbox {
|
||||
border: solid 2px darkgray;
|
||||
border-radius: 10px;
|
||||
margin: 1em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#backdrop {
|
||||
display: none;
|
||||
&.active {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 999;
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
cursor: wait;
|
||||
}
|
||||
}
|
||||
|
||||
#backdrop.active {
|
||||
}
|
||||
|
||||
#feedback {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
left: 50%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#success, #error {
|
||||
position: relative;
|
||||
left: -50%;
|
||||
border-radius: 10px;
|
||||
padding: 0.5em 1em;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#success {
|
||||
background: lightgreen;
|
||||
border: solid 2px green;
|
||||
color: green;
|
||||
}
|
||||
|
||||
#error {
|
||||
background: lightcoral;
|
||||
border: solid 2px red;
|
||||
color: red;
|
||||
}
|
||||
|
||||
body.initial {
|
||||
.popup-body {
|
||||
transition: initial;
|
||||
}
|
||||
}
|
||||
.popup {
|
||||
position: fixed;
|
||||
top: 10vh;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0px);
|
||||
max-width: max(90vw, 800px);
|
||||
max-height: 80vh;
|
||||
margin: auto;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
div.close {
|
||||
position: absolute;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
cursor: pointer;
|
||||
.icon {
|
||||
background-color: white;
|
||||
box-shadow: 0px 2px 4px 0px rgb(0 0 0 / 34%);
|
||||
}
|
||||
}
|
||||
.popup-body {
|
||||
max-height: 80vh;
|
||||
max-width: 80vw;
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
transition: transform 1s cubic-bezier(0.500, 0.250, 0.300, 1.650);
|
||||
background-color: white;
|
||||
padding: 1em;
|
||||
border-radius: 5px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
.popup-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: start;
|
||||
text-align: justify;
|
||||
max-height: inherit;
|
||||
}
|
||||
.popup-footer {
|
||||
position: relative;
|
||||
text-align: justify;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-around;
|
||||
gap: 2em;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
&.shown {
|
||||
pointer-events: initial;
|
||||
transition: transform 1s cubic-bezier(0.500, 0.250, 0.300, 1.650);
|
||||
display: block;
|
||||
.popup-body {
|
||||
transform: rotate3d(1, 0, 0, 0deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clickable {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
225
view-webapp/src/main/sass/tour.scss
Normal file
225
view-webapp/src/main/sass/tour.scss
Normal file
@@ -0,0 +1,225 @@
|
||||
@layer pairgoth {
|
||||
/* general rules */
|
||||
|
||||
.steps .step:not(.active) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.tab-content {
|
||||
display: none;
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* information section */
|
||||
|
||||
form {
|
||||
input, select, .edit {
|
||||
display: none;
|
||||
}
|
||||
&.edit {
|
||||
input:not(.hidden), select:not(.hidden), .edit:not(.hidden) {
|
||||
display: initial;
|
||||
}
|
||||
.info, #edit {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
div.field:not(.hidden) {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: space-around;
|
||||
margin: 1px;
|
||||
background-color: #eeeeee;
|
||||
&.centered {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.inline.fields {
|
||||
background-color: #eeeeee;
|
||||
margin-left: -0.5em;
|
||||
margin-right: -0.5em;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
.centered.field > label {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* registration section */
|
||||
|
||||
#player {
|
||||
&.create {
|
||||
.edition {
|
||||
display: none;
|
||||
}
|
||||
#unregister {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&.edit {
|
||||
.creation {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#player-form {
|
||||
&:not(.add) {
|
||||
#search-form, #search-result {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
#search-form {
|
||||
position: relative;
|
||||
.toggle {
|
||||
cursor: pointer;
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.checkbox {
|
||||
width: 50px;
|
||||
height: 26px;
|
||||
border-radius: 18px;
|
||||
background-color: #F7D6A3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
cursor: pointer;
|
||||
.circle {
|
||||
background-color: #6B5E8A;
|
||||
transform: translateX(0px);
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transition: 300ms;
|
||||
}
|
||||
}
|
||||
input:checked + .checkbox {
|
||||
background-color: rgb(218, 114, 80);
|
||||
.circle {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#search-result {
|
||||
position: absolute;
|
||||
background-color: gray;
|
||||
z-index: 2;
|
||||
width:100%;
|
||||
top: 100%;
|
||||
padding: 1em;
|
||||
overflow-y: auto;
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
.result-line {
|
||||
cursor: pointer;
|
||||
&:hover, &.highlighted {
|
||||
background-color: rgba(100,200,255,200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#player.popup {
|
||||
min-width: 65vw;
|
||||
}
|
||||
|
||||
/* pairing section */
|
||||
|
||||
#pairing-content {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
#pairing-lists {
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
gap: 1em;
|
||||
align-items: start;
|
||||
}
|
||||
.multi-select {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
min-height: 30vh;
|
||||
max-height: 60vh;
|
||||
min-width: 20vw;
|
||||
max-width: 40vw;
|
||||
border: solid 2px darkgray;
|
||||
border-radius: 5px;
|
||||
padding: 1em 0.5em;
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: attr(title);
|
||||
top: -0.5em;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0px);
|
||||
white-space: nowrap;
|
||||
font-size: smaller;
|
||||
font-weight: bold;
|
||||
}
|
||||
.listitem {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
gap: 1em;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
background-color: rgba(50, 50, 50, .2);
|
||||
}
|
||||
&.selected {
|
||||
background-color: rgba(100,200,255,200);
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
}
|
||||
#pairing-buttons {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: start;
|
||||
align-items: stretch;
|
||||
gap: 1em;
|
||||
}
|
||||
#unpairables {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
min-height: 10vh;
|
||||
max-height: 30vh;
|
||||
min-width: 50vw;
|
||||
}
|
||||
.choose-round.button {
|
||||
padding: 0.2em 0.8em;
|
||||
}
|
||||
|
||||
/* results section */
|
||||
|
||||
#results-list {
|
||||
text-align: center;
|
||||
.player, .result {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: rgba(0,0,0,.05);
|
||||
color: rgba(0,0,0,.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,31 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="${request.lang}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Pairgoth</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Pairgoth Go Paring Engine">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<link rel="stylesheet" href="/lib/fork-awesome-1.2.0/fork-awesome.min.css">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<script type="text/javascript" src="/js/domhelper.js"></script>
|
||||
<script type="text/javascript">
|
||||
// #[[
|
||||
let initFunctions = [];
|
||||
function onLoad(fct) {
|
||||
if (typeof(fct) == "function") initFunctions.push(fct);
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initFunctions.forEach(fct => {
|
||||
fct();
|
||||
});
|
||||
});
|
||||
// ]]#
|
||||
</script>
|
||||
</head>
|
||||
<body class="vert flex">
|
||||
<body class="vert flex initial">
|
||||
#* Debugging code to list all web context properties
|
||||
<blockquote>
|
||||
#foreach($attr in $application.getAttributeNames())
|
||||
<div>$attr = $application.getAttribute($attr)</div>
|
||||
#end
|
||||
</blockquote>
|
||||
*#
|
||||
<div id="header" class="horz flex">
|
||||
<div id="logo">
|
||||
<img src="/img/logo.svg"/>
|
||||
<img src="/img/logov2.svg"/>
|
||||
</div>
|
||||
<div id="title">
|
||||
</div>
|
||||
<div id="lang">
|
||||
<i class="$translate.flags[$request.lang] flag"></i>
|
||||
<div id="lang-list">
|
||||
#foreach($lang in $translate.flags.entrySet())
|
||||
#if($lang != $request.lang)
|
||||
<a class="lang" data-lang="$lang.key" href="#"><i class="$lang.value flag"></i> $lang.key</a>
|
||||
#end
|
||||
#end
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="center">
|
||||
@@ -37,15 +47,38 @@
|
||||
<div id="version">pairgoth v0.1</div>
|
||||
<div id="contact"><a href="mailto:pairgoth@jeudego.org">contact</a></div>
|
||||
</div>
|
||||
<div id="feedback">
|
||||
<div id="success" class="hidden"></div>
|
||||
<div id="error" class="hidden"></div>
|
||||
</div>
|
||||
<div id="backdrop"></div>
|
||||
<div id="dimmer"></div>
|
||||
<script type="text/javascript" src="/js/store2-2.14.2.min.js"></script>
|
||||
<script type="text/javascript" src="/js/tablesort-5.4.0.min.js"></script>
|
||||
<script type="text/javascript" src="/js/formproxy.js"></script>
|
||||
<div id="lang-list">
|
||||
#foreach($lang in $translate.flags.entrySet())
|
||||
#if($lang != $request.lang)
|
||||
<a class="lang" data-lang="$lang.key" href="#"><i class="$lang.value flag"></i> $lang.key</a>
|
||||
#end
|
||||
#end
|
||||
</div>
|
||||
<script type="text/javascript" src="/lib/store2-2.14.2.min.js"></script>
|
||||
<script type="text/javascript" src="/lib/tablesort-5.4.0/tablesort.min.js"></script>
|
||||
<script type="text/javascript" src="/lib/tablesort-5.4.0/sorts/tablesort.number.min.js"></script>
|
||||
<link rel="stylesheet" href="/lib/tablesort-5.4.0/tablesort.css"/>
|
||||
<script type="text/javascript" src="/lib/imaskjs-7.1.3/imask.min.js"></script>
|
||||
<script type="text/javascript" src="/js/api.js"></script>
|
||||
<script type="text/javascript" src="/js/main.js"></script>
|
||||
<script type="text/javascript" src="/js/domhelper.js"></script>
|
||||
<link rel="stylesheet" href="/lib/fork-awesome-1.2.0/fork-awesome.min.css"/>
|
||||
<link rel="stylesheet" href="/css/main.css"/>
|
||||
#if($css)
|
||||
<link rel="stylesheet" href="$css"/>
|
||||
#end
|
||||
<script type="text/javascript">
|
||||
const lang = '${request.lang}';
|
||||
const locale = '${request.locale}';
|
||||
// #[[
|
||||
onLoad(() => {
|
||||
$('body').removeClass('initial');
|
||||
$('#lang').on('click', e => {
|
||||
$('#lang-list').toggleClass('shown');
|
||||
});
|
||||
@@ -59,7 +92,26 @@
|
||||
$('#lang-list').removeClass('shown');
|
||||
}
|
||||
});
|
||||
$('.popup .popup-footer .close').on('click', e => {
|
||||
let popup = e.target.closest('.popup');
|
||||
if (popup) {
|
||||
popup.classList.remove('shown');
|
||||
$('body').removeClass('dimmed');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// syntaxic sugar for IMask
|
||||
NodeList.prototype.imask = function(options) {
|
||||
this.forEach(function (elem, i) {
|
||||
elem.imask(options);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
HTMLInputElement.prototype.imask = function(options) {
|
||||
IMask(this, options);
|
||||
}
|
||||
|
||||
// ]]#
|
||||
</script>
|
||||
</body>
|
||||
|
1
view-webapp/src/main/webapp/WEB-INF/logger.properties
Normal file
1
view-webapp/src/main/webapp/WEB-INF/logger.properties
Normal file
@@ -0,0 +1 @@
|
||||
level = info
|
@@ -1,7 +1,19 @@
|
||||
# webapp
|
||||
# environment
|
||||
env = dev
|
||||
webapp.url = https://localhost:8080
|
||||
api.url = https://localhost:8085/api
|
||||
|
||||
# webapp connector
|
||||
webapp.protocol = http
|
||||
webapp.interface = localhost
|
||||
webapp.port = 8080
|
||||
webapp.context = /
|
||||
webapp.external.url = http://localhost:8080
|
||||
|
||||
# api connector
|
||||
api.protocol = http
|
||||
api.interface = localhost
|
||||
api.port = 8085
|
||||
api.context = /api
|
||||
api.external.url = http://localhost:8085/api
|
||||
|
||||
# store
|
||||
store = file
|
||||
|
@@ -3,14 +3,15 @@
|
||||
|
||||
<toolbox scope="application">
|
||||
<tool key="translate" class="org.jeudego.pairgoth.view.TranslationTool"/>
|
||||
<tool key="strings" class="org.apache.commons.lang3.StringUtils"/>
|
||||
<tool key="utils" class="org.jeudego.pairgoth.view.PairgothTool"/>
|
||||
<!--
|
||||
<tool key="number" format="#0.00"/>
|
||||
<tool key="date" locale="fr_FR" format="yyyy-MM-dd"/>
|
||||
<tool key="inflector" class="org.atteo.evo.inflector.English"/>
|
||||
<tool key="strings" class="org.apache.commons.lang3.StringUtils"/>
|
||||
-->
|
||||
</toolbox>
|
||||
|
||||
Exception
|
||||
<toolbox scope="session">
|
||||
<!--
|
||||
<tool key="oauth" class="org.jeudego.egc2024.tool.OAuthTool"/>
|
||||
@@ -19,6 +20,7 @@
|
||||
|
||||
<toolbox scope="request">
|
||||
<tool key="api" class="org.jeudego.pairgoth.view.ApiTool"/>
|
||||
<tool key="countries" class="org.jeudego.pairgoth.view.CountriesTool"/>
|
||||
</toolbox>
|
||||
|
||||
</tools>
|
||||
|
@@ -59,6 +59,18 @@
|
||||
<load-on-startup>1</load-on-startup>
|
||||
<async-supported>true</async-supported>
|
||||
</servlet>
|
||||
<servlet>
|
||||
<servlet-name>api</servlet-name>
|
||||
<servlet-class>org.jeudego.pairgoth.web.ApiServlet</servlet-class>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
<async-supported>true</async-supported>
|
||||
</servlet>
|
||||
<servlet>
|
||||
<servlet-name>search</servlet-name>
|
||||
<servlet-class>org.jeudego.pairgoth.web.SearchServlet</servlet-class>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
<async-supported>true</async-supported>
|
||||
</servlet>
|
||||
|
||||
<!-- servlet mappings -->
|
||||
<servlet-mapping>
|
||||
@@ -69,10 +81,30 @@
|
||||
<servlet-name>sse</servlet-name>
|
||||
<url-pattern>/events/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
<servlet-mapping>
|
||||
<servlet-name>api</servlet-name>
|
||||
<url-pattern>/api/tour/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
<servlet-mapping>
|
||||
<servlet-name>search</servlet-name>
|
||||
<url-pattern>/api/search/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- context params -->
|
||||
<context-param>
|
||||
<param-name>webapp-slf4j-logger.format</param-name>
|
||||
<param-value>%logger [%level] [%ip] %message @%file:%line:%column</param-value>
|
||||
</context-param>
|
||||
<context-param>
|
||||
<param-name>org.apache.velocity.tools.loadDefaults</param-name>
|
||||
<param-value>true</param-value>
|
||||
</context-param>
|
||||
<context-param>
|
||||
<param-name>org.apache.velocity.tools.cleanConfiguration</param-name>
|
||||
<param-value>true</param-value>
|
||||
</context-param>
|
||||
<context-param>
|
||||
<param-name>org.apache.velocity.tools.userCanOverwriteTools</param-name>
|
||||
<param-value>false</param-value>
|
||||
</context-param>
|
||||
</web-app>
|
||||
|
BIN
view-webapp/src/main/webapp/fonts/Hornbill-Black.otf
Normal file
BIN
view-webapp/src/main/webapp/fonts/Hornbill-Black.otf
Normal file
Binary file not shown.
BIN
view-webapp/src/main/webapp/fonts/Hornbill-BlackItalic.otf
Normal file
BIN
view-webapp/src/main/webapp/fonts/Hornbill-BlackItalic.otf
Normal file
Binary file not shown.
BIN
view-webapp/src/main/webapp/fonts/Hornbill-Bold.otf
Normal file
BIN
view-webapp/src/main/webapp/fonts/Hornbill-Bold.otf
Normal file
Binary file not shown.
BIN
view-webapp/src/main/webapp/fonts/Hornbill-BoldItalic.otf
Normal file
BIN
view-webapp/src/main/webapp/fonts/Hornbill-BoldItalic.otf
Normal file
Binary file not shown.
BIN
view-webapp/src/main/webapp/fonts/Hornbill-Italic.otf
Normal file
BIN
view-webapp/src/main/webapp/fonts/Hornbill-Italic.otf
Normal file
Binary file not shown.
BIN
view-webapp/src/main/webapp/fonts/Hornbill-Regular.otf
Normal file
BIN
view-webapp/src/main/webapp/fonts/Hornbill-Regular.otf
Normal file
Binary file not shown.
42
view-webapp/src/main/webapp/fonts/hornbill.css
Normal file
42
view-webapp/src/main/webapp/fonts/hornbill.css
Normal file
@@ -0,0 +1,42 @@
|
||||
@font-face {
|
||||
font-family: Hornbill;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(Hornbill-Regular.otf) format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Hornbill;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(Hornbill-Italic.otf) format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Hornbill;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(Hornbill-Bold.otf) format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Hornbill;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(Hornbill-BoldItalic.otf) format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Hornbill;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url(Hornbill-Black.otf) format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Hornbill;
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
src: url(Hornbill-BlackItalic.otf) format("opentype");
|
||||
}
|
||||
|
1
view-webapp/src/main/webapp/img/logov2.svg
Normal file
1
view-webapp/src/main/webapp/img/logov2.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.6 KiB |
43
view-webapp/src/main/webapp/index-ffg.html
Normal file
43
view-webapp/src/main/webapp/index-ffg.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<div class="section">
|
||||
<h1>Welcome to <span class="logo">pairgoth</span>, your Go Pairing Engine!</h1>
|
||||
<h2>What is <span class="logo">pairgoth</span>?</h2>
|
||||
<p>At its core, <span class="logo">pairgoth</span> is a versatile Go tournament pairing engine designed to make your tournament experience effortless. <span class="logo">pairgoth</span> is the successor of <a href="https://github.com/lucvannier/opengotha">opengotha</a>, the well known pairing system software developed by <a href="http://vannier.info/jeux/accueil.htm">Luc Vannier</a> and uses the same algorithm internally, as well as import and export features towards its format.</p>
|
||||
<p><span class="logo">pairgoth</span> version 1.0-BETA supports the <b>Swiss</b></a> pairing system, ideal for championships with no handicap games, as well as the <b>MacMahon</b> pairing system, more suited for classical tournaments and cups.</p>
|
||||
<p>Future versions will support more pairing systems and more features. <a href="mailto:pairgoth@jeudego.org">Your feedback is welcome!</a></p>
|
||||
<h2>How to use <span class="logo">pairgoth</span>?</h2>
|
||||
<p>We offer you the flexibility to use <span class="logo">pairgoth</span> in a way that best suits your needs. Here are your options:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p><b>Stay in the browser</b>: If you prefer convenience, you can simply use the <span class="logo">pairgoth</span> instance graciously hosted by the French Go Federation.</p>
|
||||
<blockquote>
|
||||
<a class="nobreak" href="https://pairgoth.jeudego.org/menu.html">Launch <span class="logo">pairgoth</span></a>
|
||||
</blockquote>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>Launch a standalone instance</b>: This mode allows you to run <span class="logo">pairgoth</span> on your local computer.</p>
|
||||
<p>That's the best option if you feel more comfortable when running locally or whenever you want to be able to do the pairing without internet. You can choose to use either the standard interface (which is meant to look a lot like opengotha) and the web interface (by launching the engine and connecting to it using a browser).</p>
|
||||
<blockquote>
|
||||
<a class="nobreak" href="https://pairgoth.jeudego.org/download.html#standalone">Download <span class="logo">pairgoth</span> standalone</a>
|
||||
</blockquote>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>Launch a pairing server</b>: This mode is the best suited for big Go events like congresses, it allows to register players, enter results and manage pairing from several workstations at once.</p>
|
||||
<blockquote>
|
||||
<a class="nobreak" href="https://pairgoth.jeudego.org/download.html#server">Download <span class="logo">pairgoth</span> client/server</a>
|
||||
</blockquote>
|
||||
</li>
|
||||
<li>
|
||||
<p><b>Compile from the sources</b>: the <span class="logo">pairgoth</span> project is fully open source, and under the very permissive <a href="https://www.apache.org/licenses/LICENSE-2.0">apache licence</a>, allowing you to tweak it in any possible way. Be sure to contribute back your enhancements!</p>
|
||||
<blockquote>
|
||||
<a class="nobreak" href="https://github.com/ffrgo/pairgoth">Browse <span class="logo">pairgoth</span> sources</a>
|
||||
</blockquote>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
const lang = '${request.lang}';
|
||||
// #[[
|
||||
onLoad(() => {
|
||||
});
|
||||
// ]]#
|
||||
</script>
|
@@ -1,72 +1,15 @@
|
||||
<div class="section">
|
||||
<button id="new" class="ui blue icon floating button">
|
||||
<a href="tour" class="ui blue icon floating button">
|
||||
<i class="fa fa-plus-square-o"></i>
|
||||
New tournament
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
#foreach($tour in $api.get('tour').entrySet())
|
||||
<div class="section">
|
||||
<button data-tour-id="$tour.key" class="ui open basic secondary white icon floating button">
|
||||
$tour
|
||||
<a href="tour?id=${tour.key}" class="ui open basic secondary white icon floating button">
|
||||
<i class="fa fa-folder-open-o"></i>
|
||||
Open $tour.value
|
||||
</button>
|
||||
Open
|
||||
</a>
|
||||
</div>
|
||||
#end
|
||||
|
||||
##
|
||||
## New Tournament dialog
|
||||
##
|
||||
|
||||
<div id="new-tournament" class="ui fullscreen modal">
|
||||
<i class="close icon"></i>
|
||||
<div class="horz flex header">
|
||||
<span>New tournament</span>
|
||||
<div class="ui ordered unstackable steps">
|
||||
<div class="active step">
|
||||
<div class="content">
|
||||
<div class="title">Infos</div>
|
||||
<div class="description">name, place and date</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="content">
|
||||
<div class="title">Type</div>
|
||||
<div class="description">teams or players, rounds</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="content">
|
||||
<div class="title">Pairing</div>
|
||||
<div class="description">pairing system</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scrolling content">
|
||||
#translate('tournament-form.inc.html')
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="ui cancel black floating button">Cancel</button>
|
||||
<button class="ui next green right labeled icon floating button">
|
||||
<i class="checkmark icon"></i>
|
||||
Next
|
||||
</button>
|
||||
</div></div>
|
||||
<script type="text/javascript">
|
||||
const lang = '${request.lang}';
|
||||
// #[[
|
||||
onLoad(() => {
|
||||
$('#new').on('click', e => {
|
||||
$('#new-tournament').modal(true);
|
||||
});
|
||||
new DateRangePicker($('#date-range')[0], {
|
||||
language: lang
|
||||
});
|
||||
$('#new-tournament .tab.segment:first-child').addClass('active');
|
||||
});
|
||||
// ]]#
|
||||
</script>
|
||||
<!-- date range picker -->
|
||||
<script type="text/javascript" src="/lib/datepicker-1.3.3/datepicker-full.min.js"></script>
|
||||
<script type="text/javascript" src="/lib/datepicker-1.3.3/locales/${request.lang}.js"></script>
|
||||
<link rel="stylesheet" href="/lib/datepicker-1.3.3/datepicker.min.css">
|
||||
|
@@ -23,6 +23,34 @@ let headers = function() {
|
||||
return ret;
|
||||
};
|
||||
|
||||
function clearFeedback() {
|
||||
$('#error')[0].innerText = '';
|
||||
$('#error, #success').addClass('hidden');
|
||||
}
|
||||
|
||||
function success() {
|
||||
$('#error')[0].innerText = '';
|
||||
$('#error').addClass('hidden');
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
console.error(message);
|
||||
$('#error')[0].innerText = message;
|
||||
$('#error').removeClass('hidden');
|
||||
}
|
||||
|
||||
function error(response) {
|
||||
const contentType = response.headers.get("content-type");
|
||||
let promise =
|
||||
(contentType && contentType.indexOf("application/json") !== -1)
|
||||
? response.json().then(json => json.error || "unknown error")
|
||||
: Promise.resolve(response.statusText);
|
||||
promise.then(message => {
|
||||
message = message.replaceAll(/([a-z])([A-Z])/g,"$1 $2").toLowerCase();
|
||||
showError(message);
|
||||
});
|
||||
}
|
||||
|
||||
let api = {
|
||||
get: (path) => fetch(base + path, {
|
||||
credentials: "same-origin",
|
||||
@@ -49,22 +77,82 @@ let api = {
|
||||
|
||||
/* then, some helpers */
|
||||
|
||||
getJson: (path) => api.get(path)
|
||||
.then(resp => {
|
||||
if (resp.ok) return resp.json();
|
||||
else throw resp.statusText;
|
||||
}),
|
||||
getJson: (path) => {
|
||||
clearFeedback();
|
||||
return api.get(path)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
}
|
||||
else throw resp;
|
||||
})
|
||||
.catch(err => {
|
||||
error(err);
|
||||
return 'error'
|
||||
});
|
||||
},
|
||||
|
||||
postJson: (path, body) => api.post(path, body)
|
||||
.then(resp => {
|
||||
if (resp.ok) return resp.json();
|
||||
else throw resp.statusText;
|
||||
}),
|
||||
postJson: (path, body) => {
|
||||
clearFeedback();
|
||||
spinner(true);
|
||||
return api.post(path, body)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
success();
|
||||
return resp.json();
|
||||
}
|
||||
else {
|
||||
throw resp;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
error(err);
|
||||
return 'error';
|
||||
})
|
||||
.finally(() => {
|
||||
spinner(false);
|
||||
});
|
||||
},
|
||||
|
||||
putJson: (path, body) => {
|
||||
clearFeedback();
|
||||
spinner(true);
|
||||
return api.put(path, body)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
success();
|
||||
return resp.json();
|
||||
}
|
||||
else throw resp;
|
||||
})
|
||||
.catch(err => {
|
||||
error(err);
|
||||
return 'error';
|
||||
})
|
||||
.finally(() => {
|
||||
spinner(false);
|
||||
});
|
||||
},
|
||||
|
||||
deleteJson: (path, body) => {
|
||||
clearFeedback();
|
||||
spinner(true);
|
||||
return api.delete(path, body)
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
success();
|
||||
return resp.json();
|
||||
}
|
||||
else throw resp;
|
||||
})
|
||||
.catch(err => {
|
||||
error(err);
|
||||
return 'error';
|
||||
})
|
||||
.finally(() => {
|
||||
spinner(false);
|
||||
});
|
||||
}
|
||||
|
||||
putJson: (path, body) => api.put(path, body)
|
||||
.then(resp => {
|
||||
if (resp.ok) return resp.json();
|
||||
else throw resp.statusText;
|
||||
})
|
||||
};
|
||||
|
||||
|
@@ -49,8 +49,8 @@ Element.prototype.toggleClass = function(className) {
|
||||
NodeList.prototype.hasClass = function(className) {
|
||||
return this.item(0).classList.contains(className);
|
||||
}
|
||||
Element.prototype.toggleClass = function(className) {
|
||||
this.classList.contains(className);
|
||||
Element.prototype.hasClass = function(className) {
|
||||
return this.classList.contains(className);
|
||||
}
|
||||
Node.prototype.offset = function() {
|
||||
let _x = 0;
|
||||
@@ -66,39 +66,130 @@ Node.prototype.offset = function() {
|
||||
NodeList.prototype.offset = function() {
|
||||
this.item(0).offset();
|
||||
}
|
||||
Element.prototype.attr = function (key) {
|
||||
return this.attributes[key].value;
|
||||
Element.prototype.attr = function (key, value) {
|
||||
if (typeof(value) === 'undefined') {
|
||||
return this.attributes[key]?.value;
|
||||
} else {
|
||||
this.setAttribute(key, value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
NodeList.prototype.attr = function(key) {
|
||||
this.item(0).attr(key);
|
||||
NodeList.prototype.attr = function(key, value) {
|
||||
if (typeof(value) === 'undefined') {
|
||||
return this.item(0).attr(key);
|
||||
} else {
|
||||
this.forEach(elem => {
|
||||
elem.attr(key, value);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
Element.prototype.data = function (key) {
|
||||
return this.attributes[`data-${key}`].value
|
||||
Element.prototype.data = function (key, value) {
|
||||
if (typeof(value) === 'undefined') {
|
||||
return this.attributes[`data-${key}`]?.value
|
||||
} else {
|
||||
this.setAttribute(`data-${key}`, value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
NodeList.prototype.data = function(key) {
|
||||
this.item(0).data(key);
|
||||
NodeList.prototype.data = function(key, value) {
|
||||
if (typeof(value) === 'undefined') {
|
||||
this.item(0).data(key);
|
||||
} else {
|
||||
this.forEach(elem => {
|
||||
elem.data(key, value);
|
||||
})
|
||||
return this;
|
||||
}
|
||||
}
|
||||
NodeList.prototype.show = function(key) {
|
||||
this.item(0).show(key);
|
||||
NodeList.prototype.show = function() {
|
||||
this.item(0).show();
|
||||
return this;
|
||||
}
|
||||
Element.prototype.show = function (key) {
|
||||
Element.prototype.show = function() {
|
||||
this.style.display = 'block';
|
||||
}
|
||||
NodeList.prototype.hide = function(key) {
|
||||
this.item(0).hide(key);
|
||||
NodeList.prototype.hide = function() {
|
||||
this.item(0).hide();
|
||||
return this;
|
||||
}
|
||||
Element.prototype.hide = function (key) {
|
||||
Element.prototype.hide = function() {
|
||||
this.style.display = 'none';
|
||||
}
|
||||
|
||||
let initFunctions = [];
|
||||
function onLoad(fct) {
|
||||
if (typeof(fct) == "function") initFunctions.push(fct);
|
||||
NodeList.prototype.text = function(txt) {
|
||||
this.item(0).text(txt);
|
||||
}
|
||||
document.on("DOMContentLoaded", () => {
|
||||
initFunctions.forEach(fct => {
|
||||
fct();
|
||||
Element.prototype.text = function(txt) {
|
||||
if (typeof(txt) === 'undefined') {
|
||||
return this.textContent;
|
||||
} else {
|
||||
this.textContent = txt;
|
||||
}
|
||||
}
|
||||
NodeList.prototype.item = function (i) {
|
||||
return this[+i || 0];
|
||||
};
|
||||
NodeList.prototype.find = function(selector) {
|
||||
let result = [];
|
||||
this.forEach(function (elem, i) {
|
||||
let partial = elem.find(selector);
|
||||
result = result.concat([...partial]);
|
||||
});
|
||||
});
|
||||
return Reflect.construct(Array, result, NodeList);
|
||||
}
|
||||
Element.prototype.find = function(selector) {
|
||||
return this.querySelectorAll(':scope ' + selector);
|
||||
}
|
||||
|
||||
NodeList.prototype.clear = function() {
|
||||
this.forEach(function (elem, i) {
|
||||
elem.clear();
|
||||
});
|
||||
return this;
|
||||
}
|
||||
Element.prototype.clear = function() {
|
||||
this.innerHTML = '';
|
||||
return this;
|
||||
}
|
||||
|
||||
/*
|
||||
TODO - conflicts with from.val(), rename one of the two
|
||||
NodeList.prototype.val = function(value) {
|
||||
this.item(0).val(value);
|
||||
}
|
||||
Element.prototype.val = function(value) {
|
||||
// TODO - check that "this" has the "value" property
|
||||
if (typeof(value) === 'undefined') {
|
||||
return this.value;
|
||||
} else {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
NodeList.prototype.focus = function() {
|
||||
let first = this.item(0);
|
||||
if (first) first.focus();
|
||||
}
|
||||
|
||||
Element.prototype.index = function(selector) {
|
||||
let i = 0;
|
||||
let child = this;
|
||||
while ((child = child.previousSibling) != null) {
|
||||
if (typeof(selector) === 'undefined' || child.nodeType === Node.ELEMENT_NODE && child.matches(selector)) {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
NodeList.prototype.filter = function(selector) {
|
||||
let result = [];
|
||||
this.forEach(elem => {
|
||||
if (elem.nodeType === Node.ELEMENT_NODE && elem.matches(selector)) {
|
||||
result.push(elem);
|
||||
}
|
||||
});
|
||||
return Reflect.construct(Array, result, NodeList);
|
||||
}
|
||||
|
||||
|
@@ -1,163 +0,0 @@
|
||||
// hack to replace .34 into 0.34 (CB TODO - upstream patch to inputmask)
|
||||
const fixNumber = (value) => isNaN(value) || !`${value}`.startsWith('.') ? value : `0.${value}`;
|
||||
|
||||
class FormProxy {
|
||||
constructor(formSelector, config) {
|
||||
this.formSelector = formSelector;
|
||||
this.config = config;
|
||||
this.config.properties = () => Object.keys(this.config).filter(k => typeof(this.config[k]) !== 'function');
|
||||
this.promises = [];
|
||||
this.dirty = false;
|
||||
|
||||
this.config.import = function(obj) {
|
||||
for (let key in obj) {
|
||||
if (key in this.config) {
|
||||
this.proxy[key] = obj[key];
|
||||
} else {
|
||||
console.warn(`ignoring property ${key}`)
|
||||
}
|
||||
}
|
||||
this.config.dirty(false);
|
||||
}.bind(this);
|
||||
|
||||
this.config.export = function() {
|
||||
let ret = {}
|
||||
this.config.properties().forEach(prop => {
|
||||
ret[prop] = this.proxy[prop];
|
||||
});
|
||||
return ret;
|
||||
}.bind(this);
|
||||
|
||||
this.config.dirty = function(value) {
|
||||
if (typeof(value) === 'undefined') return thisProxy.dirty;
|
||||
else thisProxy.dirty = Boolean(value);
|
||||
}.bind(this);
|
||||
|
||||
this.config.valid = function() {
|
||||
return $(`${thisProxy.formSelector} [required]:invalid`).length === 0
|
||||
}.bind(this);
|
||||
|
||||
this.config.reset = function() {
|
||||
this.initialize();
|
||||
}.bind(this);
|
||||
|
||||
// CB TODO - needs a function to wait for promises coming from dependencies
|
||||
|
||||
this.setState('loading');
|
||||
$(() => this.configure.bind(this)());
|
||||
|
||||
let thisProxy = this;
|
||||
return this.proxy = new Proxy(config, {
|
||||
get(target, prop) {
|
||||
if (typeof(target[prop]) === 'function') {
|
||||
return target[prop];
|
||||
}
|
||||
else {
|
||||
let elem = config[prop];
|
||||
if (typeof(elem) === 'undefined') throw `invalid property: ${prop}`
|
||||
return elem.getter();
|
||||
}
|
||||
},
|
||||
set(target, prop, value) {
|
||||
let def = config[prop];
|
||||
if (typeof(def) === 'undefined') throw `invalid property: ${prop}`
|
||||
let depends = [].concat(def.depends ? def.depends : []);
|
||||
let proms = depends.flatMap(field => config[field].promise).filter(prom => prom);
|
||||
let operation = () => {
|
||||
def.setter(value);
|
||||
if (typeof(def.change) === 'function') {
|
||||
let rst = def.change(value, def.elem);
|
||||
if (typeof(rst?.then) === 'function') {
|
||||
def.promise = rst;
|
||||
}
|
||||
}
|
||||
};
|
||||
if (proms.length) Promise.all(proms).then(() => operation());
|
||||
else operation();
|
||||
config.dirty(true);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
configure() {
|
||||
this.form = $(this.formSelector);
|
||||
if (!this.form.length) throw `Form not found: ${this.formSelector}`;
|
||||
this.form.on('submit', e => { e.preventDefault(); return false; });
|
||||
let controls = this.form.find('input[name],select[name],textarea[name]');
|
||||
controls.on('input change keyup', e => {
|
||||
this.setState('editing');
|
||||
this.config.dirty(true);
|
||||
});
|
||||
controls.each((i,e) => {
|
||||
let name = $(e).attr('name');
|
||||
if (!(name in this.config)) this.config[name] = {};
|
||||
});
|
||||
this.config.properties().forEach(key => {
|
||||
let def = this.config[key];
|
||||
if (!def) def = this.config[key] = {};
|
||||
else if (typeof(def) === 'function') return true; // continue foreach
|
||||
if (!def.getter) {
|
||||
let elem = def.elem;
|
||||
if (!elem || !elem.length) elem = $(`${this.formSelector} [name="${key}"]`);
|
||||
if (!elem || !elem.length) elem = $(`#${key}`);
|
||||
if (!elem || !elem.length) throw `element not found: ${key}`;
|
||||
def.elem = elem;
|
||||
def.getter = elem.is('input,select,textarea')
|
||||
? elem.attr('type') === 'radio'
|
||||
? (() => elem.filter(':checked').val())
|
||||
: (() => elem.data('default')
|
||||
? elem.val()
|
||||
? elem.is('.number')
|
||||
? elem.val().replace(/ /g, '')
|
||||
: elem.val()
|
||||
: elem.data('default')
|
||||
: elem.is('.number')
|
||||
? elem.val() ? elem.val().replace(/ /g, '') : elem.val()
|
||||
: elem.val())
|
||||
: (() => elem.text());
|
||||
def.setter = elem.is('input,select,textarea')
|
||||
? elem.attr('type') === 'radio'
|
||||
? (value => elem.filter(`[value="${value}"]`).prop('checked', true))
|
||||
: elem.is('input.number') ? (value => elem.val(fixNumber(value))) : (value => elem.val(value))
|
||||
: (value => elem.text(value));
|
||||
if (typeof(def.change) === 'function') {
|
||||
elem.on('change', () => def.change(def.getter(), elem));
|
||||
}
|
||||
}
|
||||
let loading = def?.loading;
|
||||
switch (typeof(loading)) {
|
||||
case 'function':
|
||||
let rst = loading(def.elem);
|
||||
if (typeof(rst?.then) === 'function') {
|
||||
this.promises.push(rst);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
Promise.all(this.promises).then(() => { this.promises = []; this.initialize(); });
|
||||
}, 10);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.config.properties().forEach(key => {
|
||||
let def = this.config[key];
|
||||
if (typeof(def.initial) === 'undefined') {
|
||||
this.proxy[key] = '';
|
||||
} else {
|
||||
if (typeof(def.initial) === 'function') {
|
||||
def.initial(def.elem)
|
||||
} else if (def.initial != null) {
|
||||
this.proxy[key] = def.initial;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.config.dirty(false);
|
||||
this.setState('initial');
|
||||
}
|
||||
|
||||
setState(state) {
|
||||
if (this.form && this.form.length) this.form[0].dispatchEvent(new Event(state));
|
||||
}
|
||||
}
|
@@ -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,21 +101,127 @@ Element.prototype.modal = function(show) {
|
||||
}
|
||||
return this;
|
||||
}
|
||||
*/
|
||||
|
||||
/* DOM helpers */
|
||||
|
||||
HTMLFormElement.prototype.val = function(name, value) {
|
||||
let hasValue = typeof(value) !== 'undefined';
|
||||
let ctl = this.find(`[name="${name}"]`)[0];
|
||||
if (!ctl) {
|
||||
console.error(`unknown input name: ${name}`)
|
||||
}
|
||||
let tag = ctl.tagName;
|
||||
let type = tag === 'INPUT' ? ctl.attr('type') : undefined;
|
||||
if (
|
||||
(tag === 'INPUT' && ['text', 'number', 'hidden'].includes(ctl.attr('type'))) ||
|
||||
tag === 'SELECT'
|
||||
) {
|
||||
if (hasValue) {
|
||||
ctl.value = value;
|
||||
return;
|
||||
}
|
||||
else return ctl.value;
|
||||
} else if (tag === 'INPUT' && ctl.attr('type') === 'radio') {
|
||||
if (hasValue) {
|
||||
ctl = $(`input[name="${name}"][value="${value}"]`);
|
||||
if (ctl) ctl.checked = true;
|
||||
return;
|
||||
} else {
|
||||
ctl = $(`input[name="${name}"]:checked`);
|
||||
if (ctl) return ctl[0].value;
|
||||
else return null;
|
||||
}
|
||||
} else if (tag === 'INPUT' && ctl.attr('type') === 'checkbox') {
|
||||
if (hasValue) {
|
||||
ctl.checked = value !== 'false' && Boolean(value);
|
||||
return;
|
||||
}
|
||||
else return ctl.checked && ctl.value ? ctl.value : ctl.checked;
|
||||
}
|
||||
console.error(`unhandled input tag or type for input ${name} (tag: ${tag}, type:${type}`);
|
||||
return null;
|
||||
};
|
||||
|
||||
function msg(id) {
|
||||
let ctl = $(`#${id}`)[0];
|
||||
return ctl.textContent;
|
||||
}
|
||||
|
||||
function spinner(show) {
|
||||
if (show) $('#backdrop').addClass('active');
|
||||
else $('#backdrop').removeClass('active');
|
||||
}
|
||||
|
||||
function modal(id) {
|
||||
$('body').addClass('dimmed');
|
||||
$(`#${id}.popup`).addClass('shown');
|
||||
}
|
||||
|
||||
function close_modal() {
|
||||
$('body').removeClass('dimmed');
|
||||
$(`.popup`).removeClass('shown');
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
/*
|
||||
document.on('click', e => {
|
||||
if (!e.target.closest('.modal')) $('.modal').hide();
|
||||
})
|
||||
$('button.close').on('click', e => {
|
||||
let modal = e.target.closest('.popup');
|
||||
if (modal) {
|
||||
modal.removeClass('shown');
|
||||
$('body').removeClass('dimmed');
|
||||
}
|
||||
});
|
||||
|
||||
/* commented for now - do we want this?
|
||||
$('#dimmer').on('click', e => $('.popup').removeClass('shown');
|
||||
*/
|
||||
$('i.close.icon').on('click', e => {
|
||||
let modal = e.target.closest('.modal');
|
||||
if (modal) modal.modal(false);
|
||||
|
||||
// keyboard handling
|
||||
document.on('keyup', e => {
|
||||
switch (e.key) {
|
||||
case 'Escape': {
|
||||
if ($('#player').hasClass('shown')) {
|
||||
if ($('#needle')[0].value) {
|
||||
$('#needle')[0].value = '';
|
||||
initSearch();
|
||||
} else {
|
||||
close_modal();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ArrowDown': {
|
||||
if (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 (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 (searchResultShown()) {
|
||||
fillPlayer(searchResult[searchHighlight]);
|
||||
} else {
|
||||
$('#register')[0].click();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
$('.modal .actions .cancel').on('click', e => {
|
||||
e.target.closest('.modal').modal(false);
|
||||
});
|
||||
$('#dimmer').on('click', e => $('.modal').modal(false));
|
||||
|
||||
});
|
||||
|
||||
|
151
view-webapp/src/main/webapp/js/tour-information.inc.js
Normal file
151
view-webapp/src/main/webapp/js/tour-information.inc.js
Normal file
@@ -0,0 +1,151 @@
|
||||
onLoad(() => {
|
||||
$('#edit').on('click', e => {
|
||||
e.preventDefault();
|
||||
$('#tournament-infos').addClass('edit');
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#cancel, #close').on('click', e => {
|
||||
e.preventDefault();
|
||||
if ($('#tournament-infos').hasClass('edit') && typeof(tour_id) !== 'undefined') {
|
||||
$('#tournament-infos').removeClass('edit')
|
||||
} else {
|
||||
document.location.href = '/index';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#validate').on('click', e => {
|
||||
let form = e.target.closest('form');
|
||||
let valid = true;
|
||||
// validate required fields
|
||||
let required = ['name', 'shortName', 'startDate', 'endDate'];
|
||||
if (!form.find('input[name="online"]')[0].checked) required.push('location')
|
||||
for (let name of required) {
|
||||
let ctl = form.find(`input[name=${name}]`)[0];
|
||||
let val = ctl.value;
|
||||
if (val) {
|
||||
ctl.setCustomValidity('');
|
||||
} else {
|
||||
valid = false;
|
||||
ctl.setCustomValidity(msg('required_field'));
|
||||
}
|
||||
}
|
||||
if (!valid) return;
|
||||
// validate short_name
|
||||
let shortNameCtl = form.find('input[name="shortName"]')[0];
|
||||
let shortName = shortNameCtl.value;
|
||||
if (safeRegex.test(shortName)) {
|
||||
shortNameCtl.setCustomValidity('');
|
||||
} else {
|
||||
valid = false;
|
||||
shortNameCtl.setCustomValidity(msg('invalid_character'));
|
||||
}
|
||||
// if (!valid) return;
|
||||
// ...
|
||||
});
|
||||
|
||||
for(let name of ['startDate', 'endDate']) {
|
||||
let control = $(`input[name="${name}"]`)[0];
|
||||
if (control.value) {
|
||||
control.value = formatDate(control.value);
|
||||
}
|
||||
}
|
||||
new DateRangePicker($('#date-range')[0], {
|
||||
autohide: true,
|
||||
language: datepickerLocale || 'en'
|
||||
});
|
||||
|
||||
$('input[name="online"]').on('change', e => {
|
||||
$('input[name="location"]')[0].disabled = e.target.checked;
|
||||
});
|
||||
|
||||
$('select[name="timeSystemType"]').on('change', e => {
|
||||
switch (e.target.value) {
|
||||
case 'CANADIAN':
|
||||
$('#increment').addClass('hidden');
|
||||
$('#maxTime').addClass('hidden');
|
||||
$('#byoyomi').removeClass('hidden');
|
||||
$('#periods').addClass('hidden');
|
||||
$('#stones').removeClass('hidden');
|
||||
break;
|
||||
case 'FISCHER':
|
||||
$('#increment').removeClass('hidden');
|
||||
$('#maxTime').removeClass('hidden');
|
||||
$('#byoyomi').addClass('hidden');
|
||||
$('#periods').addClass('hidden');
|
||||
$('#stones').addClass('hidden');
|
||||
break;
|
||||
case 'STANDARD':
|
||||
$('#increment').addClass('hidden');
|
||||
$('#maxTime').addClass('hidden');
|
||||
$('#byoyomi').removeClass('hidden');
|
||||
$('#periods').removeClass('hidden');
|
||||
$('#stones').addClass('hidden');
|
||||
break;
|
||||
case 'SUDDEN_DEATH':
|
||||
$('#increment').addClass('hidden');
|
||||
$('#maxTime').addClass('hidden');
|
||||
$('#byoyomi').addClass('hidden');
|
||||
$('#periods').addClass('hidden');
|
||||
$('#stones').addClass('hidden');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$('input.duration').imask({
|
||||
mask: '00:00:00',
|
||||
lazy: false,
|
||||
overwrite: true
|
||||
});
|
||||
|
||||
$('#tournament-infos').on('submit', e => {
|
||||
e.preventDefault();
|
||||
let form = e.target;
|
||||
let tour = {
|
||||
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: form.val('pairing'),
|
||||
// mmFloor: form.val('mmFloor'),
|
||||
mmBar: form.val('mmBar'),
|
||||
main: {
|
||||
firstSeed: form.val('firstSeed'),
|
||||
secondSeed: form.val('secondSeed')
|
||||
},
|
||||
handicap: {
|
||||
correction: -form.val('correction'),
|
||||
treshold: form.val('treshold')
|
||||
}
|
||||
},
|
||||
timeSystem: {
|
||||
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);
|
||||
if (typeof(tour_id) !== 'undefined') {
|
||||
api.putJson(`tour/${tour_id}`, tour)
|
||||
.then(tour => {
|
||||
window.location.reload();
|
||||
});
|
||||
} else {
|
||||
api.postJson('tour', tour)
|
||||
.then(tour => {
|
||||
window.location.href += `?id=${tour.id}`;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
51
view-webapp/src/main/webapp/js/tour-pairing.inc.js
Normal file
51
view-webapp/src/main/webapp/js/tour-pairing.inc.js
Normal file
@@ -0,0 +1,51 @@
|
||||
let focused = undefined;
|
||||
|
||||
function pair(parts) {
|
||||
api.postJson(`tour/${tour_id}/pair/${activeRound}`, parts)
|
||||
.then(rst => {
|
||||
if (rst !== 'error') {
|
||||
document.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function unpair(games) {
|
||||
api.deleteJson(`tour/${tour_id}/pair/${activeRound}`, games)
|
||||
.then(rst => {
|
||||
if (rst !== 'error') {
|
||||
document.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onLoad(()=>{
|
||||
$('.listitem').on('click', e => {
|
||||
if (e.shiftKey && typeof(focused) !== 'undefined') {
|
||||
let from = focused.index('.listitem');
|
||||
let to = e.target.closest('.listitem').index('.listitem');
|
||||
if (from > to) {
|
||||
let tmp = from;
|
||||
from = to;
|
||||
to = tmp;
|
||||
}
|
||||
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'));
|
||||
}
|
||||
});
|
||||
$('#pair').on('click', e => {
|
||||
let parts = $('#pairables')[0].childNodes.filter('.selected.listitem').map(item => parseInt(item.data("id")));
|
||||
pair(parts);
|
||||
});
|
||||
$('#unpair').on('click', e => {
|
||||
let games = $('#paired')[0].childNodes.filter('.selected.listitem').map(item => parseInt(item.data("id")));
|
||||
unpair(games);
|
||||
});
|
||||
});
|
214
view-webapp/src/main/webapp/js/tour-registration.inc.js
Normal file
214
view-webapp/src/main/webapp/js/tour-registration.inc.js
Normal file
@@ -0,0 +1,214 @@
|
||||
const SEARCH_DELAY = 100;
|
||||
let searchTimer = undefined;
|
||||
let resultTemplate;
|
||||
let searchResult;
|
||||
let searchHighlight;
|
||||
|
||||
function initSearch() {
|
||||
let needle = $('#needle')[0].value.trim();
|
||||
if (searchTimer) {
|
||||
clearTimeout(searchTimer);
|
||||
}
|
||||
searchTimer = setTimeout(() => {
|
||||
search(needle);
|
||||
}, SEARCH_DELAY);
|
||||
}
|
||||
|
||||
function searchResultShown() {
|
||||
return !(typeof(searchHighlight) === 'undefined' || !searchResult || !searchResult.length || typeof(searchResult[searchHighlight]) === 'undefined')
|
||||
}
|
||||
|
||||
function search(needle) {
|
||||
needle = needle.trim();
|
||||
if (needle && needle.length > 2) {
|
||||
let form = $('#player-form')[0];
|
||||
let search = {
|
||||
needle: needle,
|
||||
aga: form.val('aga'),
|
||||
egf: form.val('egf'),
|
||||
ffg: form.val('ffg'),
|
||||
}
|
||||
let country = form.val('countryFilter');
|
||||
if (country) search.countryFilter = country;
|
||||
let searchFormState = {
|
||||
countryFilter: country ? true : false,
|
||||
aga: search.aga,
|
||||
egf: search.egf,
|
||||
ffg: search.ffg
|
||||
};
|
||||
store('searchFormState', searchFormState);
|
||||
api.postJson('search', search)
|
||||
.then(result => {
|
||||
if (Array.isArray(result)) {
|
||||
searchResult = result
|
||||
let html = resultTemplate.render(result);
|
||||
$('#search-result')[0].innerHTML = html;
|
||||
} else console.log(result);
|
||||
})
|
||||
} else {
|
||||
$('#search-result').clear();
|
||||
searchTimer = undefined;
|
||||
searchResult = undefined;
|
||||
searchHighlight = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function parseRank(rank) {
|
||||
let groups = /(\d+)([kd])/.exec(rank)
|
||||
if (groups) {
|
||||
let level = parseInt(groups[1]);
|
||||
let letter = groups[2];
|
||||
switch (letter) {
|
||||
case 'k': return -level;
|
||||
case 'd': return level - 1;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function fillPlayer(player) {
|
||||
let form = $('#player-form')[0];
|
||||
form.val('name', player.name);
|
||||
form.val('firstname', player.firstname);
|
||||
form.val('country', player.country.toLowerCase());
|
||||
form.val('club', player.club);
|
||||
form.val('rank', parseRank(player.rank));
|
||||
form.val('rating', player.rating);
|
||||
$('#needle')[0].value = '';
|
||||
initSearch();
|
||||
$('#register').focus();
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
$('input.numeric').imask({
|
||||
mask: Number,
|
||||
scale: 0,
|
||||
min: 0,
|
||||
max: 4000
|
||||
});
|
||||
new Tablesort($('#players')[0]);
|
||||
$('#add').on('click', e => {
|
||||
let form = $('#player-form')[0];
|
||||
form.addClass('add');
|
||||
// $('#player-form input.participation').forEach(chk => chk.checked = true);
|
||||
form.reset();
|
||||
$('#player').removeClass('edit').addClass('create');
|
||||
modal('player');
|
||||
$('#needle').focus();
|
||||
});
|
||||
$('#cancel-register').on('click', e => {
|
||||
e.preventDefault();
|
||||
close_modal();
|
||||
searchHighlight = undefined;
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#register').on('click', e => {
|
||||
let form = e.target.closest('form');
|
||||
let valid = true;
|
||||
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')[0].requestSubmit() not working?!
|
||||
$('#player-form')[0].dispatchEvent(new CustomEvent('submit', {cancelable: true}));
|
||||
});
|
||||
$('#player-form').on('submit', e => {
|
||||
("submitting!!")
|
||||
e.preventDefault();
|
||||
let form = $('#player-form')[0];
|
||||
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'),
|
||||
skip: form.find('input.participation').map((input,i) => [i+1, input.checked]).filter(arr => !arr[1]).map(arr => arr[0])
|
||||
}
|
||||
if (form.hasClass('add')) {
|
||||
api.postJson(`tour/${tour_id}/part`, player)
|
||||
.then(player => {
|
||||
if (player !== 'error') {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let id = form.val('id');
|
||||
player['id'] = id;
|
||||
api.putJson(`tour/${tour_id}/part/${id}`, player)
|
||||
.then(player => {
|
||||
if (player !== 'error') {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
$('#players > tbody > tr').on('click', e => {
|
||||
let id = e.target.closest('tr').attr('data-id');
|
||||
api.getJson(`tour/${tour_id}/part/${id}`)
|
||||
.then(player => {
|
||||
if (player !== 'error') {
|
||||
let form = $('#player-form')[0];
|
||||
form.val('id', player.id);
|
||||
form.val('name', player.name);
|
||||
form.val('firstname', player.firstname);
|
||||
form.val('rating', player.rating);
|
||||
form.val('rank', player.rank);
|
||||
form.val('country', player.country);
|
||||
form.val('club', player.club);
|
||||
for (r = 1; r <= tour_rounds; ++r) {
|
||||
form.val(`r${r}`, !(player.skip && player.skip.includes(r)));
|
||||
}
|
||||
form.removeClass('add');
|
||||
$('#player').removeClass('create').addClass('edit');
|
||||
modal('player');
|
||||
}
|
||||
});
|
||||
});
|
||||
resultTemplate = jsrender.templates($('#result')[0]);
|
||||
$('#needle').on('input', e => {
|
||||
initSearch();
|
||||
});
|
||||
$('#clear-search').on('click', e => {
|
||||
$('#needle')[0].value = '';
|
||||
$('#search-result').clear();
|
||||
});
|
||||
let searchFromState = store('searchFormState')
|
||||
if (searchFromState) {
|
||||
for (let id of ["countryFilter", "aga", "egf", "ffg"]) {
|
||||
$(`#${id}`)[0].checked = searchFromState[id];
|
||||
}
|
||||
}
|
||||
$('.toggle').on('click', e => {
|
||||
let chk = e.target.closest('.toggle');
|
||||
let checkbox = chk.find('input')[0];
|
||||
checkbox.checked = !checkbox.checked;
|
||||
initSearch();
|
||||
});
|
||||
document.on('click', e => {
|
||||
let resultLine = e.target.closest('.result-line');
|
||||
if (resultLine) {
|
||||
let index = e.target.closest('.result-line').data('index');
|
||||
fillPlayer(searchResult[index]);
|
||||
}
|
||||
});
|
||||
$('#unregister').on('click', e => {
|
||||
let form = $('#player-form')[0];
|
||||
let id = form.val('id');
|
||||
api.deleteJson(`tour/${tour_id}/part/${id}`)
|
||||
.then(ret => {
|
||||
if (ret !== 'error') {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
3
view-webapp/src/main/webapp/js/tour-results.inc.js
Normal file
3
view-webapp/src/main/webapp/js/tour-results.inc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
onLoad(()=>{
|
||||
new Tablesort($('#results-table')[0]);
|
||||
});
|
1
view-webapp/src/main/webapp/lib/README.md
Normal file
1
view-webapp/src/main/webapp/lib/README.md
Normal file
@@ -0,0 +1 @@
|
||||
date picker: https://mymth.github.io/vanillajs-datepicker/#/
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
||||
New locales for the datepicker can be downloaded from [the datepicker repository](https://github.com/mymth/vanillajs-datepicker/tree/master/dist/js/locales)
|
||||
|
3075
view-webapp/src/main/webapp/lib/datepicker-1.3.4/datepicker-full.js
Normal file
3075
view-webapp/src/main/webapp/lib/datepicker-1.3.4/datepicker-full.js
Normal file
File diff suppressed because it is too large
Load Diff
1
view-webapp/src/main/webapp/lib/datepicker-1.3.4/datepicker-full.min.js
vendored
Normal file
1
view-webapp/src/main/webapp/lib/datepicker-1.3.4/datepicker-full.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
317
view-webapp/src/main/webapp/lib/datepicker-1.3.4/datepicker.css
Normal file
317
view-webapp/src/main/webapp/lib/datepicker-1.3.4/datepicker.css
Normal file
@@ -0,0 +1,317 @@
|
||||
.datepicker {
|
||||
width: -moz-min-content;
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.datepicker:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.datepicker-dropdown {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.datepicker-dropdown.datepicker-orient-top {
|
||||
padding-top: 0;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.datepicker-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 4px;
|
||||
background-color: hsl(0, 0%, 100%);
|
||||
}
|
||||
|
||||
.datepicker-dropdown .datepicker-picker {
|
||||
box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);
|
||||
}
|
||||
|
||||
.datepicker-main {
|
||||
flex: auto;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.datepicker-footer {
|
||||
box-shadow: inset 0 1px 1px rgba(10, 10, 10, 0.1);
|
||||
background-color: hsl(0, 0%, 96%);
|
||||
}
|
||||
|
||||
.datepicker-title {
|
||||
box-shadow: inset 0 -1px 1px rgba(10, 10, 10, 0.1);
|
||||
background-color: hsl(0, 0%, 96%);
|
||||
padding: 0.375rem 0.75rem;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.datepicker-controls {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.datepicker-header .datepicker-controls {
|
||||
padding: 2px 2px 0;
|
||||
}
|
||||
|
||||
.datepicker-controls .button {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
border: 1px solid gainsboro;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
background-color: hsl(0, 0%, 100%);
|
||||
cursor: pointer;
|
||||
padding: calc(0.375em - 1px) 0.75em;
|
||||
height: 2.25em;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
white-space: nowrap;
|
||||
color: hsl(0, 0%, 21%);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.datepicker-controls .button:focus,
|
||||
.datepicker-controls .button:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.datepicker-controls .button:hover {
|
||||
border-color: #b8b8b8;
|
||||
color: hsl(0, 0%, 21%);
|
||||
}
|
||||
|
||||
.datepicker-controls .button:focus {
|
||||
border-color: hsl(217, 71%, 53%);
|
||||
color: hsl(0, 0%, 21%);
|
||||
}
|
||||
|
||||
.datepicker-controls .button:focus:not(:active) {
|
||||
box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25);
|
||||
}
|
||||
|
||||
.datepicker-controls .button:active {
|
||||
border-color: #474747;
|
||||
color: hsl(0, 0%, 21%);
|
||||
}
|
||||
|
||||
.datepicker-controls .button[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.datepicker-header .datepicker-controls .button {
|
||||
border-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.datepicker-header .datepicker-controls .button:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.datepicker-header .datepicker-controls .button:active {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.datepicker-footer .datepicker-controls .button {
|
||||
flex: auto;
|
||||
margin: calc(0.375rem - 1px) 0.375rem;
|
||||
border-radius: 2px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.datepicker-controls .view-switch {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.datepicker-controls .prev-button,
|
||||
.datepicker-controls .next-button {
|
||||
padding-right: 0.375rem;
|
||||
padding-left: 0.375rem;
|
||||
flex: 0 0 14.2857142857%;
|
||||
}
|
||||
|
||||
.datepicker-controls .prev-button.disabled,
|
||||
.datepicker-controls .next-button.disabled {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.datepicker-view,
|
||||
.datepicker-grid {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.datepicker-view {
|
||||
align-items: stretch;
|
||||
width: 15.75rem;
|
||||
}
|
||||
|
||||
.datepicker-grid {
|
||||
flex-wrap: wrap;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.datepicker .days {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.datepicker .days-of-week {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.datepicker .week-numbers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 9.6774193548%;
|
||||
}
|
||||
|
||||
.datepicker .weeks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.datepicker span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.datepicker .dow {
|
||||
height: 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.datepicker .week {
|
||||
flex: auto;
|
||||
color: #b8b8b8;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.datepicker-cell,
|
||||
.datepicker .days .dow {
|
||||
flex-basis: 14.2857142857%;
|
||||
}
|
||||
|
||||
.datepicker-cell {
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
.datepicker-cell:not(.day) {
|
||||
flex-basis: 25%;
|
||||
height: 4.5rem;
|
||||
}
|
||||
|
||||
.datepicker-cell:not(.disabled):hover {
|
||||
background-color: #f9f9f9;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.datepicker-cell.focused:not(.selected) {
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
|
||||
.datepicker-cell.selected,
|
||||
.datepicker-cell.selected:hover {
|
||||
background-color: hsl(217, 71%, 53%);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.datepicker-cell.disabled {
|
||||
color: gainsboro;
|
||||
}
|
||||
|
||||
.datepicker-cell.prev:not(.disabled),
|
||||
.datepicker-cell.next:not(.disabled) {
|
||||
color: hsl(0, 0%, 48%);
|
||||
}
|
||||
|
||||
.datepicker-cell.prev.selected,
|
||||
.datepicker-cell.next.selected {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
.datepicker-cell.highlighted:not(.selected):not(.range):not(.today) {
|
||||
border-radius: 0;
|
||||
background-color: hsl(0, 0%, 96%);
|
||||
}
|
||||
|
||||
.datepicker-cell.highlighted:not(.selected):not(.range):not(.today):not(.disabled):hover {
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
.datepicker-cell.highlighted:not(.selected):not(.range):not(.today).focused {
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
|
||||
.datepicker-cell.today:not(.selected) {
|
||||
background-color: hsl(171, 100%, 41%);
|
||||
}
|
||||
|
||||
.datepicker-cell.today:not(.selected):not(.disabled) {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.datepicker-cell.today.focused:not(.selected) {
|
||||
background-color: #00ccad;
|
||||
}
|
||||
|
||||
.datepicker-cell.range-end:not(.selected),
|
||||
.datepicker-cell.range-start:not(.selected) {
|
||||
background-color: #b8b8b8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.datepicker-cell.range-end.focused:not(.selected),
|
||||
.datepicker-cell.range-start.focused:not(.selected) {
|
||||
background-color: #b3b3b3;
|
||||
}
|
||||
|
||||
.datepicker-cell.range-start:not(.range-end) {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.datepicker-cell.range-end:not(.range-start) {
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.datepicker-cell.range {
|
||||
border-radius: 0;
|
||||
background-color: gainsboro;
|
||||
}
|
||||
|
||||
.datepicker-cell.range:not(.disabled):not(.focused):not(.today):hover {
|
||||
background-color: #d7d7d7;
|
||||
}
|
||||
|
||||
.datepicker-cell.range.disabled {
|
||||
color: #c6c6c6;
|
||||
}
|
||||
|
||||
.datepicker-cell.range.focused {
|
||||
background-color: #d1d1d1;
|
||||
}
|
||||
|
||||
.datepicker-input.in-edit {
|
||||
border-color: #276bda;
|
||||
}
|
||||
|
||||
.datepicker-input.in-edit:focus,
|
||||
.datepicker-input.in-edit:active {
|
||||
box-shadow: 0 0 0.25em 0.25em rgba(39, 107, 218, 0.2);
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Arabic-Algeria translation for bootstrap-datepicker
|
||||
* Rabah Saadi <infosrabah@gmail.com>
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales['ar-DZ'] = {
|
||||
days: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت", "الأحد"],
|
||||
daysShort: ["أحد", "اثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت", "أحد"],
|
||||
daysMin: ["ح", "ن", "ث", "ع", "خ", "ج", "س", "ح"],
|
||||
months: ["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],
|
||||
monthsShort: ["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],
|
||||
today: "هذا اليوم",
|
||||
rtl: true,
|
||||
monthsTitle: "أشهر",
|
||||
clear: "إزالة",
|
||||
format: "yyyy/mm/dd",
|
||||
weekStart: 0
|
||||
};
|
||||
}());
|
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Arabic-Tunisia translation for bootstrap-datepicker
|
||||
* Souhaieb Besbes <besbes.souhaieb@gmail.com>
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales['ar-tn'] = {
|
||||
days: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت", "الأحد"],
|
||||
daysShort: ["أحد", "اثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت", "أحد"],
|
||||
daysMin: ["ح", "ن", "ث", "ع", "خ", "ج", "س", "ح"],
|
||||
months: ["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],
|
||||
monthsShort: ["جانفي","فيفري","مارس","أفريل","ماي","جوان","جويليه","أوت","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],
|
||||
today: "هذا اليوم",
|
||||
rtl: true
|
||||
};
|
||||
}());
|
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Arabic translation for bootstrap-datepicker
|
||||
* Mohammed Alshehri <alshehri866@gmail.com>
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales.ar = {
|
||||
days: ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة", "السبت", "الأحد"],
|
||||
daysShort: ["أحد", "اثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت", "أحد"],
|
||||
daysMin: ["ح", "ن", "ث", "ع", "خ", "ج", "س", "ح"],
|
||||
months: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
|
||||
monthsShort: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
|
||||
today: "هذا اليوم",
|
||||
rtl: true
|
||||
};
|
||||
}());
|
@@ -0,0 +1,14 @@
|
||||
// Azerbaijani
|
||||
(function () {
|
||||
Datepicker.locales.az = {
|
||||
days: ["Bazar", "Bazar ertəsi", "Çərşənbə axşamı", "Çərşənbə", "Cümə axşamı", "Cümə", "Şənbə"],
|
||||
daysShort: ["B.", "B.e", "Ç.a", "Ç.", "C.a", "C.", "Ş."],
|
||||
daysMin: ["B.", "B.e", "Ç.a", "Ç.", "C.a", "C.", "Ş."],
|
||||
months: ["Yanvar", "Fevral", "Mart", "Aprel", "May", "İyun", "İyul", "Avqust", "Sentyabr", "Oktyabr", "Noyabr", "Dekabr"],
|
||||
monthsShort: ["Yan", "Fev", "Mar", "Apr", "May", "İyun", "İyul", "Avq", "Sen", "Okt", "Noy", "Dek"],
|
||||
today: "Bu gün",
|
||||
weekStart: 1,
|
||||
clear: "Təmizlə",
|
||||
monthsTitle: 'Aylar'
|
||||
};
|
||||
}());
|
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Bulgarian translation for bootstrap-datepicker
|
||||
* Apostol Apostolov <apostol.s.apostolov@gmail.com>
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales.bg = {
|
||||
days: ["Неделя", "Понеделник", "Вторник", "Сряда", "Четвъртък", "Петък", "Събота"],
|
||||
daysShort: ["Нед", "Пон", "Вто", "Сря", "Чет", "Пет", "Съб"],
|
||||
daysMin: ["Н", "П", "В", "С", "Ч", "П", "С"],
|
||||
months: ["Януари", "Февруари", "Март", "Април", "Май", "Юни", "Юли", "Август", "Септември", "Октомври", "Ноември", "Декември"],
|
||||
monthsShort: ["Ян", "Фев", "Мар", "Апр", "Май", "Юни", "Юли", "Авг", "Сеп", "Окт", "Ное", "Дек"],
|
||||
today: "днес"
|
||||
};
|
||||
}());
|
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Bamanankan (bm) translation for bootstrap-datepicker
|
||||
* Fatou Fall <fatou@medicmobile.org>
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales.bm = {
|
||||
days: ["Kari","Ntɛnɛn","Tarata","Araba","Alamisa","Juma","Sibiri"],
|
||||
daysShort: ["Kar","Ntɛ","Tar","Ara","Ala","Jum","Sib"],
|
||||
daysMin: ["Ka","Nt","Ta","Ar","Al","Ju","Si"],
|
||||
months: ["Zanwuyekalo","Fewuruyekalo","Marisikalo","Awirilikalo","Mɛkalo","Zuwɛnkalo","Zuluyekalo","Utikalo","Sɛtanburukalo","ɔkutɔburukalo","Nowanburukalo","Desanburukalo"],
|
||||
monthsShort: ["Zan","Few","Mar","Awi","Mɛ","Zuw","Zul","Uti","Sɛt","ɔku","Now","Des"],
|
||||
today: "Bi",
|
||||
monthsTitle: "Kalo",
|
||||
clear: "Ka jɔsi",
|
||||
weekStart: 1,
|
||||
format: "dd/mm/yyyy"
|
||||
};
|
||||
}());
|
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Bengali (Bangla) translation for bootstrap-datepicker
|
||||
* Karim Khan <kkhancse91@gmail.com>
|
||||
* Orif N. Jr. <orif.zade@gmail.com>
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales.bn = {
|
||||
days: ["রবিবার","সোমবার","মঙ্গলবার","বুধবার","বৃহস্পতিবার","শুক্রবার","শনিবার"],
|
||||
daysShort: ["রবিবার","সোমবার","মঙ্গলবার","বুধবার","বৃহস্পতিবার","শুক্রবার","শনিবার"],
|
||||
daysMin: ["রবি","সোম","মঙ্গল","বুধ","বৃহস্পতি","শুক্র","শনি"],
|
||||
months: ["জানুয়ারী","ফেব্রুয়ারি","মার্চ","এপ্রিল","মে","জুন","জুলাই","অগাস্ট","সেপ্টেম্বর","অক্টোবর","নভেম্বর","ডিসেম্বর"],
|
||||
monthsShort: ["জানুয়ারী","ফেব্রুয়ারি","মার্চ","এপ্রিল","মে","জুন","জুলাই","অগাস্ট","সেপ্টেম্বর","অক্টোবর","নভেম্বর","ডিসেম্বর"],
|
||||
today: "আজ",
|
||||
monthsTitle: "মাস",
|
||||
clear: "পরিষ্কার",
|
||||
weekStart: 0,
|
||||
format: "mm/dd/yyyy"
|
||||
};
|
||||
}());
|
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Breton translation for bootstrap-datepicker
|
||||
* Gwenn Meynier <tornoz@laposte.net>
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales.br = {
|
||||
days: ["Sul", "Lun", "Meurzh", "Merc'her", "Yaou", "Gwener", "Sadorn"],
|
||||
daysShort: ["Sul", "Lun", "Meu.", "Mer.", "Yao.", "Gwe.", "Sad."],
|
||||
daysMin: ["Su", "L", "Meu", "Mer", "Y", "G", "Sa"],
|
||||
months: ["Genver", "C'hwevrer", "Meurzh", "Ebrel", "Mae", "Mezheven", "Gouere", "Eost", "Gwengolo", "Here", "Du", "Kerzu"],
|
||||
monthsShort: ["Genv.", "C'hw.", "Meur.", "Ebre.", "Mae", "Mezh.", "Goue.", "Eost", "Gwen.", "Here", "Du", "Kerz."],
|
||||
today: "Hiziv",
|
||||
monthsTitle: "Miz",
|
||||
clear: "Dilemel",
|
||||
weekStart: 1,
|
||||
format: "dd/mm/yyyy"
|
||||
};
|
||||
}());
|
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Bosnian translation for bootstrap-datepicker
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales.bs = {
|
||||
days: ["Nedjelja","Ponedjeljak", "Utorak", "Srijeda", "Četvrtak", "Petak", "Subota"],
|
||||
daysShort: ["Ned", "Pon", "Uto", "Sri", "Čet", "Pet", "Sub"],
|
||||
daysMin: ["N", "Po", "U", "Sr", "Č", "Pe", "Su"],
|
||||
months: ["Januar", "Februar", "Mart", "April", "Maj", "Juni", "Juli", "August", "Septembar", "Oktobar", "Novembar", "Decembar"],
|
||||
monthsShort: ["Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec"],
|
||||
today: "Danas",
|
||||
weekStart: 1,
|
||||
format: "dd.mm.yyyy"
|
||||
};
|
||||
}());
|
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Catalan translation for bootstrap-datepicker
|
||||
* J. Garcia <jogaco.en@gmail.com>
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales.ca = {
|
||||
days: ["diumenge", "dilluns", "dimarts", "dimecres", "dijous", "divendres", "dissabte"],
|
||||
daysShort: ["dg.", "dl.", "dt.", "dc.", "dj.", "dv.", "ds."],
|
||||
daysMin: ["dg", "dl", "dt", "dc", "dj", "dv", "ds"],
|
||||
months: ["gener", "febrer", "març", "abril", "maig", "juny", "juliol", "agost", "setembre", "octubre", "novembre", "desembre"],
|
||||
monthsShort: ["gen.", "febr.", "març", "abr.", "maig", "juny", "jul.", "ag.", "set.", "oct.", "nov.", "des."],
|
||||
today: "Avui",
|
||||
monthsTitle: "Mesos",
|
||||
clear: "Esborra",
|
||||
weekStart: 1,
|
||||
format: "dd/mm/yyyy"
|
||||
};
|
||||
}());
|
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Czech translation for bootstrap-datepicker
|
||||
* Matěj Koubík <matej@koubik.name>
|
||||
* Fixes by Michal Remiš <michal.remis@gmail.com>
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales.cs = {
|
||||
days: ["Neděle", "Pondělí", "Úterý", "Středa", "Čtvrtek", "Pátek", "Sobota"],
|
||||
daysShort: ["Ned", "Pon", "Úte", "Stř", "Čtv", "Pát", "Sob"],
|
||||
daysMin: ["Ne", "Po", "Út", "St", "Čt", "Pá", "So"],
|
||||
months: ["Leden", "Únor", "Březen", "Duben", "Květen", "Červen", "Červenec", "Srpen", "Září", "Říjen", "Listopad", "Prosinec"],
|
||||
monthsShort: ["Led", "Úno", "Bře", "Dub", "Kvě", "Čer", "Čnc", "Srp", "Zář", "Říj", "Lis", "Pro"],
|
||||
today: "Dnes",
|
||||
clear: "Vymazat",
|
||||
monthsTitle: "Měsíc",
|
||||
weekStart: 1,
|
||||
format: "dd.mm.yyyy"
|
||||
};
|
||||
}());
|
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Welsh translation for bootstrap-datepicker
|
||||
* S. Morris <s.morris@bangor.ac.uk>
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales.cy = {
|
||||
days: ["Sul", "Llun", "Mawrth", "Mercher", "Iau", "Gwener", "Sadwrn"],
|
||||
daysShort: ["Sul", "Llu", "Maw", "Mer", "Iau", "Gwe", "Sad"],
|
||||
daysMin: ["Su", "Ll", "Ma", "Me", "Ia", "Gwe", "Sa"],
|
||||
months: ["Ionawr", "Chewfror", "Mawrth", "Ebrill", "Mai", "Mehefin", "Gorfennaf", "Awst", "Medi", "Hydref", "Tachwedd", "Rhagfyr"],
|
||||
monthsShort: ["Ion", "Chw", "Maw", "Ebr", "Mai", "Meh", "Gor", "Aws", "Med", "Hyd", "Tach", "Rha"],
|
||||
today: "Heddiw"
|
||||
};
|
||||
}());
|
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Danish translation for bootstrap-datepicker
|
||||
* Christian Pedersen <https: //github.com/chripede>
|
||||
* Ivan Mylyanyk <https: //github.com/imylyanyk>
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales.da = {
|
||||
days: ["Søndag", "Mandag", "Tirsdag", "Onsdag", "Torsdag", "Fredag", "Lørdag"],
|
||||
daysShort: ["Søn", "Man", "Tir", "Ons", "Tor", "Fre", "Lør"],
|
||||
daysMin: ["Sø", "Ma", "Ti", "On", "To", "Fr", "Lø"],
|
||||
months: ["Januar", "Februar", "Marts", "April", "Maj", "Juni", "Juli", "August", "September", "Oktober", "November", "December"],
|
||||
monthsShort: ["Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec"],
|
||||
today: "I Dag",
|
||||
weekStart: 1,
|
||||
clear: "Nulstil",
|
||||
format: "dd/mm/yyyy",
|
||||
monthsTitle: "Måneder"
|
||||
};
|
||||
}());
|
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* German translation for bootstrap-datepicker
|
||||
* Sam Zurcher <sam@orelias.ch>
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales.de = {
|
||||
days: ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"],
|
||||
daysShort: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"],
|
||||
daysMin: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"],
|
||||
months: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"],
|
||||
monthsShort: ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"],
|
||||
today: "Heute",
|
||||
monthsTitle: "Monate",
|
||||
clear: "Löschen",
|
||||
weekStart: 1,
|
||||
format: "dd.mm.yyyy"
|
||||
};
|
||||
}());
|
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Greek translation for bootstrap-datepicker
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales.el = {
|
||||
days: ["Κυριακή", "Δευτέρα", "Τρίτη", "Τετάρτη", "Πέμπτη", "Παρασκευή", "Σάββατο"],
|
||||
daysShort: ["Κυρ", "Δευ", "Τρι", "Τετ", "Πεμ", "Παρ", "Σαβ"],
|
||||
daysMin: ["Κυ", "Δε", "Τρ", "Τε", "Πε", "Πα", "Σα"],
|
||||
months: ["Ιανουάριος", "Φεβρουάριος", "Μάρτιος", "Απρίλιος", "Μάιος", "Ιούνιος", "Ιούλιος", "Αύγουστος", "Σεπτέμβριος", "Οκτώβριος", "Νοέμβριος", "Δεκέμβριος"],
|
||||
monthsShort: ["Ιαν", "Φεβ", "Μαρ", "Απρ", "Μάι", "Ιουν", "Ιουλ", "Αυγ", "Σεπ", "Οκτ", "Νοε", "Δεκ"],
|
||||
today: "Σήμερα",
|
||||
clear: "Καθαρισμός",
|
||||
weekStart: 1,
|
||||
format: "d/m/yyyy"
|
||||
};
|
||||
}());
|
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Australian English translation for bootstrap-datepicker
|
||||
* Steve Chapman <steven.p.chapman@gmail.com>
|
||||
*/
|
||||
(function () {
|
||||
Datepicker.locales['en-AU'] = {
|
||||
days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
|
||||
daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
|
||||
daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
|
||||
months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
|
||||
monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
||||
today: "Today",
|
||||
monthsTitle: "Months",
|
||||
clear: "Clear",
|
||||
weekStart: 1,
|
||||
format: "d/mm/yyyy"
|
||||
};
|
||||
}());
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user