list all pairings of all rounds of a swiss tournament

pull/8454/head
Thibault Duplessis 2021-03-22 18:04:49 +01:00
parent 40fe1f8772
commit 642c14cb74
20 changed files with 165 additions and 53 deletions

View File

@ -82,6 +82,18 @@ final class Swiss(
private def isCtxInTheTeam(teamId: lila.team.Team.ID)(implicit ctx: Context) =
ctx.userId.??(u => env.team.cached.teamIds(u).dmap(_ contains teamId))
def round(id: String, round: Int) =
Open { implicit ctx =>
OptionFuResult(env.swiss.api.byId(SwissId(id))) { swiss =>
(round > 0 && round <= swiss.round.value).option(lila.swiss.SwissRound.Number(round)) ?? { r =>
val page = getInt("page").filter(0.<)
env.swiss.roundPager(swiss, r, page | 0) map { pager =>
Ok(html.swiss.show.round(swiss, r, pager))
}
}
}
}
def form(teamId: String) =
Open { implicit ctx =>
Ok(html.swiss.form.create(env.swiss.forms.create, teamId)).fuccess

View File

@ -6,8 +6,6 @@ import lila.common.paginator.Paginator
trait PaginatorHelper {
implicit def toRichPager[A](pager: Paginator[A]): RichPager = new RichPager(pager)
def pagerNext(pager: lila.common.paginator.Paginator[_], url: Int => String): Option[Tag] =
pager.nextPage.map { np =>
div(cls := "pager")(pagerA(url(np)))
@ -20,23 +18,3 @@ trait PaginatorHelper {
private def pagerA(url: String) = a(rel := "next", href := url)("Next")
}
final class RichPager(pager: Paginator[_]) {
def sliding(length: Int, showPost: Boolean = true): List[Option[Int]] = {
val fromPage = 1 max (pager.currentPage - length)
val toPage = pager.nbPages min (pager.currentPage + length)
val pre = fromPage match {
case 1 => Nil
case 2 => List(1.some)
case _ => List(1.some, none)
}
val post = toPage match {
case x if x == pager.nbPages => Nil
case x if x == pager.nbPages - 1 => List(pager.nbPages.some)
case _ if showPost => List(none, pager.nbPages.some)
case _ => List(none)
}
pre ::: (fromPage to toPage).view.map(some).toList ::: post
}
}

View File

@ -129,11 +129,6 @@ trait UserHelper { self: I18nHelper with StringHelper with NumberHelper =>
modIcon = false
)
def userIdLink(
userId: String,
cssClass: Option[String]
)(implicit lang: Lang): Frag = userIdLink(userId.some, cssClass)
def titleTag(title: Option[Title]): Option[Frag] =
title map { t =>
frag(userTitleTag(t), nbsp)

View File

@ -1,10 +1,15 @@
package views.html.base
package views.html
package base
import chess.format.FEN
import controllers.routes
import play.api.i18n.Lang
import play.api.mvc.Call
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.paginator.Paginator
object bits {
@ -47,4 +52,43 @@ z-index: 99;
def fenAnalysisLink(fen: FEN)(implicit lang: Lang) =
a(href := routes.UserAnalysis.parseArg(fen.value.replace(" ", "_")))(trans.analysis())
def paginationByQuery(route: Call, pager: Paginator[_], showPost: Boolean): Option[Frag] =
pagination(page => s"$route?page=$page", pager, showPost)
def pagination(url: Int => String, pager: Paginator[_], showPost: Boolean): Option[Frag] =
pager.hasToPaginate option pagination(url, pager.currentPage, pager.nbPages, showPost)
def pagination(url: Int => String, page: Int, nbPages: Int, showPost: Boolean): Tag =
st.nav(cls := "pagination")(
if (page > 1) a(href := url(page - 1), dataIcon := "I")
else span(cls := "disabled", dataIcon := "I"),
sliding(page, nbPages, 3, showPost = showPost).map {
case None => raw(" &hellip; ")
case Some(p) if p == page => span(cls := "current")(p)
case Some(p) => a(href := url(p))(p)
},
if (page < nbPages) a(rel := "next", href := url(page + 1), dataIcon := "H")
else span(cls := "disabled", dataIcon := "H")
)
private def sliding(pager: Paginator[_], length: Int, showPost: Boolean): List[Option[Int]] =
sliding(pager.currentPage, pager.nbPages, length, showPost)
private def sliding(page: Int, nbPages: Int, length: Int, showPost: Boolean): List[Option[Int]] = {
val fromPage = 1 max (page - length)
val toPage = nbPages.min(page + length)
val pre = fromPage match {
case 1 => Nil
case 2 => List(1.some)
case _ => List(1.some, none)
}
val post = toPage match {
case x if x == nbPages => Nil
case x if x == nbPages - 1 => List(nbPages.some)
case _ if showPost => List(none, nbPages.some)
case _ => List(none)
}
pre ::: (fromPage to toPage).view.map(Some.apply).toList ::: post
}
}

View File

@ -1,12 +1,9 @@
package views.html
package forum
import play.api.mvc.Call
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.paginator.Paginator
import controllers.routes
@ -19,21 +16,6 @@ object bits {
)
)
def pagination(route: Call, pager: Paginator[_], showPost: Boolean) =
pager.hasToPaginate option {
def url(page: Int) = s"$route?page=$page"
st.nav(cls := "pagination")(
if (pager.hasPreviousPage) a(href := url(pager.previousPage.get), dataIcon := "I")
else span(cls := "disabled", dataIcon := "I"),
pager.sliding(3, showPost = showPost).map {
case None => raw(" &hellip; ")
case Some(p) if p == pager.currentPage => span(cls := "current")(p)
case Some(p) => a(href := url(p))(p)
},
if (pager.hasNextPage) a(rel := "next", href := url(pager.nextPage.get), dataIcon := "H")
else span(cls := "disabled", dataIcon := "H")
)
}
private[forum] val dataTopic = attr("data-topic")
private[forum] val dataUnsub = attr("data-unsub")
}

View File

@ -70,7 +70,7 @@ object categ {
)
)
val bar = div(cls := "bar")(
bits.pagination(routes.ForumCateg.show(categ.slug, 1), topics, showPost = false),
views.html.base.bits.paginationByQuery(routes.ForumCateg.show(categ.slug, 1), topics, showPost = false),
newTopicButton
)

View File

@ -93,7 +93,8 @@ object topic {
.some
) {
val teamOnly = categ.team.filterNot(myTeam)
val pager = bits.pagination(routes.ForumTopic.show(categ.slug, topic.slug, 1), posts, showPost = true)
val pager = views.html.base.bits
.paginationByQuery(routes.ForumTopic.show(categ.slug, topic.slug, 1), posts, showPost = true)
main(cls := "forum forum-topic page-small box box-pad")(
h1(

View File

@ -9,9 +9,14 @@ import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.String.html.safeJsonValue
import lila.swiss.{ Swiss, SwissCondition }
import lila.swiss.SwissRound
import lila.common.paginator.Paginator
import lila.swiss.SwissPairing
object show {
private def fullName(s: Swiss) = s"${s.name} by ${teamIdToName(s.teamId)}"
def apply(
s: Swiss,
verdicts: SwissCondition.All.WithVerdicts,
@ -23,7 +28,7 @@ object show {
val isDirector = ctx.userId.has(s.createdBy)
val hasScheduleInput = isDirector && s.settings.manualRounds && s.isNotFinished
views.html.base.layout(
title = s"${s.name} #${s.id}",
title = fullName(s),
moreJs = frag(
jsModule("swiss"),
hasScheduleInput option jsModule("flatpickr"),
@ -54,7 +59,7 @@ object show {
chessground = false,
openGraph = lila.app.ui
.OpenGraph(
title = s"${s.name}: ${s.variant.name} ${s.clock.show} #${s.id}",
title = s"${fullName(s)}: ${s.variant.name} ${s.clock.show} #${s.id}",
url = s"$netBaseUrl${routes.Swiss.show(s.id.value).url}",
description =
s"${s.nbPlayers} players compete in the ${showEnglishDate(s.startsAt)} ${s.name} swiss tournament " +
@ -73,4 +78,36 @@ object show {
)
)
}
def round(s: Swiss, r: SwissRound.Number, pairings: Paginator[SwissPairing])(implicit ctx: Context) =
views.html.base.layout(
title = s"${fullName(s)} • Round $r/${s.round}",
moreCss = cssTag("swiss.show"),
moreJs = infiniteScrollTag
) {
val pager = views.html.base.bits
.pagination(p => routes.Swiss.round(s.id.value, p).url, r.value, s.round.value, showPost = true)
main(cls := "box swiss__round")(
h1(
a(href := routes.Swiss.show(s.id.value))(s.name),
s" • Round $r/${s.round}"
),
pager(cls := "pagination--top"),
table(cls := "slist slist-pad")(
tbody(cls := "infinite-scroll")(
pairings.currentPageResults map { p =>
tr(cls := "paginated")(
td(a(href := routes.Round.watcher(p.gameId, "white"), cls := "glpt")(s"#${p.gameId}")),
td(userIdLink(p.white.some)),
td(p strResultOf chess.White),
td(p strResultOf chess.Black),
td(userIdLink(p.black.some))
)
},
pagerNextTable(pairings, p => routes.Swiss.round(s.id.value, r.value).url)
)
),
pager(cls := "pagination--bottom")
)
}
}

View File

@ -279,6 +279,7 @@ GET /swiss controllers.Swiss.home
GET /swiss/new/:teamId controllers.Swiss.form(teamId: String)
POST /swiss/new/:teamId controllers.Swiss.create(teamId: String)
GET /swiss/$id<\w{8}> controllers.Swiss.show(id: String)
GET /swiss/$id<\w{8}>/round/:round controllers.Swiss.round(id: String, round: Int)
GET /swiss/$id<\w{8}>.trf controllers.Swiss.exportTrf(id: String)
POST /swiss/$id<\w{8}>/join controllers.Swiss.join(id: String)
POST /swiss/$id<\w{8}>/withdraw controllers.Swiss.withdraw(id: String)

View File

@ -54,6 +54,8 @@ final class Env(
val api: SwissApi = wire[SwissApi]
lazy val roundPager = wire[SwissRoundPager]
private def teamOf = api.teamOf _
private lazy val socket = wire[SwissSocket]

View File

@ -24,6 +24,7 @@ case class SwissPairing(
def whiteWins = status == Right(Some(Color.White))
def blackWins = status == Right(Some(Color.Black))
def isDraw = status == Right(None)
def strResultOf(color: Color) = status.fold(_ => "*", _.fold("1/2")(c => if (c == color) "1" else "0"))
}
object SwissPairing {

View File

@ -0,0 +1,31 @@
package lila.swiss
import reactivemongo.api.ReadPreference
import scala.concurrent.ExecutionContext
import lila.common.config
import lila.common.paginator.Paginator
import lila.db.dsl._
import lila.db.paginator.Adapter
final class SwissRoundPager(colls: SwissColls)(implicit ec: ExecutionContext) {
import BsonHandlers._
private val maxPerPage = config.MaxPerPage(50)
def apply(swiss: Swiss, round: SwissRound.Number, page: Int): Fu[Paginator[SwissPairing]] =
Paginator(
adapter = new Adapter[SwissPairing](
collection = colls.pairing,
selector = SwissPairing.fields { f =>
$doc(f.swissId -> swiss.id, f.round -> round)
},
projection = none,
sort = $empty,
readPreference = ReadPreference.secondaryPreferred
),
currentPage = page,
maxPerPage = maxPerPage
)
}

View File

@ -1,4 +1,4 @@
.forum .pagination {
.pagination {
color: $c-font-dimmer;
display: flex;

View File

@ -1,5 +1,6 @@
@import '../../../common/css/plugin';
@import '../../../common/css/component/slist';
@import '../../../common/css/component/pagination';
@import '../../../common/css/form/form3';
@import '../../../common/css/form/cmn-toggle';
@import '../../../common/css/form/captcha';

View File

@ -3,7 +3,6 @@
@import 'post';
@import 'completion';
@import 'table';
@import 'pagination';
@import 'mention';
@import 'reactions';

View File

@ -0,0 +1,10 @@
.pagination {
margin-left: var(--box-padding);
&--top {
padding-bottom: 1em;
border-bottom: $border;
}
&--bottom {
padding: 1em 0;
}
}

View File

@ -11,6 +11,7 @@ $mq-col3: $mq-col3-uniboard;
@import 'app-standing';
@import 'stats';
@import 'player-info';
@import 'round-pairings';
.swiss {
.pull-quote {

View File

@ -4,15 +4,16 @@
background: $c-bg-box;
padding: 1.7em 0;
align-self: flex-start;
text-align: center;
h2 {
font-size: 1.5em;
margin-bottom: 1em;
text-align: center;
}
table {
margin: auto;
text-align: left;
}
td {
@ -21,4 +22,8 @@
text-align: right;
line-height: 2em;
}
&__links {
margin-top: 2em;
}
}

View File

@ -7,5 +7,6 @@
@import '../../../common/css/component/context-streamer';
@import '../../../common/css/component/now-playing';
@import '../../../common/css/component/podium';
@import '../../../common/css/component/pagination';
@import '../../../chat/css/chat';
@import '../show';

View File

@ -244,6 +244,17 @@ function stats(ctrl: SwissCtrl): VNode | undefined {
numberRow(noarg('byes'), [s.byes, slots], 'percent'),
numberRow(noarg('absences'), [s.absences, slots], 'percent'),
]),
h('div.swiss__stats__links', [
h(
'a',
{
attrs: {
href: `/swiss/${ctrl.data.id}/round/1`,
},
},
`View all ${ctrl.data.round} rounds`
),
]),
])
: undefined;
}