offer swiss results download as CSV - for #8869
parent
6957c0fc16
commit
c8e280c51c
|
@ -301,20 +301,22 @@ final class Api(
|
|||
}
|
||||
}
|
||||
|
||||
def swissResults(id: String) =
|
||||
Action.async { implicit req =>
|
||||
env.swiss.api byId lila.swiss.Swiss.Id(id) flatMap {
|
||||
_ ?? { swiss =>
|
||||
jsonStream {
|
||||
env.swiss.api
|
||||
.resultStream(swiss, MaxPerSecond(50), getInt("nb", req) | Int.MaxValue)
|
||||
.mapAsync(8) { case (player, rank) =>
|
||||
env.swiss.json.playerResult(player, rank.toInt)
|
||||
}
|
||||
}.fuccess
|
||||
}
|
||||
def swissResults(id: String) = Action.async { implicit req =>
|
||||
val csv = HTTPRequest.acceptsCsv(req) || get("as", req).has("csv")
|
||||
env.swiss.api byId lila.swiss.Swiss.Id(id) map {
|
||||
_ ?? { swiss =>
|
||||
val source = env.swiss.api
|
||||
.resultStream(swiss, MaxPerSecond(50), getInt("nb", req) | Int.MaxValue)
|
||||
.mapAsync(8) { p =>
|
||||
env.user.lightUserApi.asyncFallback(p.player.userId) map p.withUser
|
||||
}
|
||||
val result =
|
||||
if (csv) csvStream(lila.swiss.SwissCsv(source))
|
||||
else jsonStream(source.map(env.swiss.json.playerResult))
|
||||
result.pipe(asAttachment(env.api.gameApiV2.filename(swiss, if (csv) "csv" else "ndjson")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def gamesByUsersStream =
|
||||
AnonOrScopedBody(parse.tolerantText)()(
|
||||
|
@ -426,21 +428,27 @@ final class Api(
|
|||
.map(some)
|
||||
.keepAlive(70.seconds, () => none) // play's idleTimeout = 75s
|
||||
|
||||
def sourceToNdJson(source: Source[JsValue, _]) =
|
||||
def sourceToNdJson(source: Source[JsValue, _]): Result =
|
||||
sourceToNdJsonString {
|
||||
source.map { o =>
|
||||
Json.stringify(o) + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
def sourceToNdJsonOption(source: Source[Option[JsValue], _]) =
|
||||
def sourceToNdJsonOption(source: Source[Option[JsValue], _]): Result =
|
||||
sourceToNdJsonString {
|
||||
source.map { _ ?? Json.stringify + "\n" }
|
||||
}
|
||||
|
||||
private def sourceToNdJsonString(source: Source[String, _]) =
|
||||
private def sourceToNdJsonString(source: Source[String, _]): Result =
|
||||
Ok.chunked(source).as(ndJsonContentType) pipe noProxyBuffer
|
||||
|
||||
def csvStream(makeSource: => Source[String, _])(implicit req: RequestHeader): Result =
|
||||
GlobalConcurrencyLimitPerIP(HTTPRequest ipAddress req)(makeSource)(sourceToCsv)
|
||||
|
||||
private def sourceToCsv(source: Source[String, _]): Result =
|
||||
Ok.chunked(source.map(_ + "\n")).as(csvContentType) pipe noProxyBuffer
|
||||
|
||||
private[controllers] val GlobalConcurrencyLimitPerIP = new lila.memo.ConcurrencyLimit[IpAddress](
|
||||
name = "API concurrency per IP",
|
||||
key = "api.ip",
|
||||
|
|
|
@ -63,6 +63,7 @@ final class GameApiV2(
|
|||
}
|
||||
|
||||
private val fileR = """[\s,]""".r
|
||||
|
||||
def filename(game: Game, format: Format): Fu[String] =
|
||||
gameLightUsers(game) map { case (wu, bu) =>
|
||||
fileR.replaceAllIn(
|
||||
|
@ -76,6 +77,7 @@ final class GameApiV2(
|
|||
"_"
|
||||
)
|
||||
}
|
||||
|
||||
def filename(tour: Tournament, format: Format): String =
|
||||
fileR.replaceAllIn(
|
||||
"lichess_tournament_%s_%s_%s.%s".format(
|
||||
|
@ -86,13 +88,17 @@ final class GameApiV2(
|
|||
),
|
||||
"_"
|
||||
)
|
||||
|
||||
def filename(swiss: lila.swiss.Swiss, format: Format): String =
|
||||
filename(swiss, format.toString.toLowerCase)
|
||||
|
||||
def filename(swiss: lila.swiss.Swiss, format: String): String =
|
||||
fileR.replaceAllIn(
|
||||
"lichess_swiss_%s_%s_%s.%s".format(
|
||||
Tag.UTCDate.format.print(swiss.startsAt),
|
||||
swiss.id,
|
||||
lila.common.String.slugify(swiss.name),
|
||||
format.toString.toLowerCase
|
||||
format
|
||||
),
|
||||
"_"
|
||||
)
|
||||
|
|
|
@ -95,6 +95,7 @@ object HTTPRequest {
|
|||
|
||||
def acceptsNdJson(req: RequestHeader) = req.headers get HeaderNames.ACCEPT contains "application/x-ndjson"
|
||||
def acceptsJson(req: RequestHeader) = req.headers get HeaderNames.ACCEPT contains "application/json"
|
||||
def acceptsCsv(req: RequestHeader) = req.headers get HeaderNames.ACCEPT contains "text/csv"
|
||||
|
||||
def actionName(req: RequestHeader): String =
|
||||
req.attrs.get(Router.Attrs.ActionName).getOrElse("NoHandler")
|
||||
|
|
|
@ -638,7 +638,7 @@ final class SwissApi(
|
|||
}
|
||||
}
|
||||
|
||||
def resultStream(swiss: Swiss, perSecond: MaxPerSecond, nb: Int): Source[(SwissPlayer, Long), _] =
|
||||
def resultStream(swiss: Swiss, perSecond: MaxPerSecond, nb: Int): Source[SwissPlayer.WithRank, _] =
|
||||
SwissPlayer.fields { f =>
|
||||
colls.player
|
||||
.find($doc(f.swissId -> swiss.id))
|
||||
|
@ -648,6 +648,9 @@ final class SwissApi(
|
|||
.documentSource(nb)
|
||||
.throttle(perSecond.value, 1 second)
|
||||
.zipWithIndex
|
||||
.map { case (player, index) =>
|
||||
SwissPlayer.WithRank(player, index.toInt + 1)
|
||||
}
|
||||
}
|
||||
|
||||
private val idNameProjection = $doc("name" -> true)
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package lila.swiss
|
||||
|
||||
import akka.stream.scaladsl.Source
|
||||
|
||||
object SwissCsv {
|
||||
|
||||
def apply(results: Source[SwissPlayer.WithUserAndRank, _]): Source[String, _] =
|
||||
Source(
|
||||
List(
|
||||
toCsv(
|
||||
"Rank",
|
||||
"Title",
|
||||
"Username",
|
||||
"Rating",
|
||||
"Score",
|
||||
"Tie Break",
|
||||
"Performance"
|
||||
)
|
||||
)
|
||||
) concat
|
||||
results.map(apply)
|
||||
|
||||
def apply(p: SwissPlayer.WithUserAndRank): String = toCsv(
|
||||
p.rank.toString,
|
||||
~p.user.title,
|
||||
p.user.name,
|
||||
p.player.rating.toString,
|
||||
p.player.score.value.toString,
|
||||
p.player.tieBreak.value.toString,
|
||||
p.player.performance.??(_.value.toString)
|
||||
)
|
||||
|
||||
private def toCsv(values: String*) = values mkString ","
|
||||
}
|
|
@ -141,8 +141,8 @@ final class SwissJson(
|
|||
}
|
||||
}
|
||||
|
||||
def playerResult(player: SwissPlayer, rank: Int): Fu[JsObject] =
|
||||
lightUserApi.asyncFallback(player.userId) map { user =>
|
||||
def playerResult(p: SwissPlayer.WithUserAndRank): JsObject = p match {
|
||||
case SwissPlayer.WithUserAndRank(player, user, rank) =>
|
||||
Json
|
||||
.obj(
|
||||
"rank" -> rank,
|
||||
|
@ -154,7 +154,7 @@ final class SwissJson(
|
|||
.add("title" -> user.title)
|
||||
.add("performance" -> player.performance)
|
||||
.add("absent" -> player.absent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object SwissJson {
|
||||
|
|
|
@ -53,13 +53,16 @@ object SwissPlayer {
|
|||
byes = Set.empty
|
||||
).recomputeScore
|
||||
|
||||
case class Ranked(rank: Int, player: SwissPlayer) {
|
||||
def is(other: Ranked) = player is other.player
|
||||
override def toString = s"$rank. ${player.userId}[${player.rating}]"
|
||||
case class WithRank(player: SwissPlayer, rank: Int) {
|
||||
def is(other: WithRank) = player is other.player
|
||||
def withUser(user: LightUser) = WithUserAndRank(player, user, rank)
|
||||
override def toString = s"$rank. ${player.userId}[${player.rating}]"
|
||||
}
|
||||
|
||||
case class WithUser(player: SwissPlayer, user: LightUser)
|
||||
|
||||
case class WithUserAndRank(player: SwissPlayer, user: LightUser, rank: Int)
|
||||
|
||||
sealed private[swiss] trait Viewish {
|
||||
val player: SwissPlayer
|
||||
val rank: Int
|
||||
|
|
|
@ -89,7 +89,7 @@ final class SwissStandingApi(
|
|||
"players" -> rankedPlayers
|
||||
.zip(users)
|
||||
.zip(sheets)
|
||||
.map { case ((SwissPlayer.Ranked(rank, player), user), sheet) =>
|
||||
.map { case ((SwissPlayer.WithRank(player, rank), user), sheet) =>
|
||||
SwissJson.playerJson(
|
||||
swiss,
|
||||
SwissPlayer.View(
|
||||
|
@ -103,16 +103,16 @@ final class SwissStandingApi(
|
|||
}
|
||||
)
|
||||
|
||||
private def bestWithRank(id: Swiss.Id, nb: Int, skip: Int): Fu[List[SwissPlayer.Ranked]] =
|
||||
private def bestWithRank(id: Swiss.Id, nb: Int, skip: Int): Fu[List[SwissPlayer.WithRank]] =
|
||||
best(id, nb, skip).map { res =>
|
||||
res
|
||||
.foldRight(List.empty[SwissPlayer.Ranked] -> (res.size + skip)) { case (p, (res, rank)) =>
|
||||
(SwissPlayer.Ranked(rank, p) :: res, rank - 1)
|
||||
.foldRight(List.empty[SwissPlayer.WithRank] -> (res.size + skip)) { case (p, (res, rank)) =>
|
||||
(SwissPlayer.WithRank(p, rank) :: res, rank - 1)
|
||||
}
|
||||
._1
|
||||
}
|
||||
|
||||
private def bestWithRankByPage(id: Swiss.Id, nb: Int, page: Int): Fu[List[SwissPlayer.Ranked]] =
|
||||
private def bestWithRankByPage(id: Swiss.Id, nb: Int, page: Int): Fu[List[SwissPlayer.WithRank]] =
|
||||
bestWithRank(id, nb, (page - 1) * nb)
|
||||
|
||||
private def best(id: Swiss.Id, nb: Int, skip: Int): Fu[List[SwissPlayer]] =
|
||||
|
|
|
@ -227,6 +227,7 @@ function stats(ctrl: SwissCtrl): VNode | undefined {
|
|||
const s = ctrl.data.stats,
|
||||
noarg = ctrl.trans.noarg,
|
||||
slots = ctrl.data.round * ctrl.data.nbPlayers;
|
||||
console.log(ctrl.data);
|
||||
return s
|
||||
? h('div.swiss__stats', [
|
||||
h('h2', noarg('tournamentComplete')),
|
||||
|
@ -281,7 +282,18 @@ function stats(ctrl: SwissCtrl): VNode | undefined {
|
|||
download: true,
|
||||
},
|
||||
},
|
||||
'Download results'
|
||||
'Download results as NDJSON'
|
||||
),
|
||||
h(
|
||||
'a.text',
|
||||
{
|
||||
attrs: {
|
||||
'data-icon': 'x',
|
||||
href: `/api/swiss/${ctrl.data.id}/results?as=csv`,
|
||||
download: true,
|
||||
},
|
||||
},
|
||||
'Download results as CSV'
|
||||
),
|
||||
h('br'),
|
||||
h(
|
||||
|
|
Loading…
Reference in New Issue