From 642c14cb74c351224d5741a51580adcf0cd4bf7d Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Mon, 22 Mar 2021 18:04:49 +0100 Subject: [PATCH] list all pairings of all rounds of a swiss tournament --- app/controllers/Swiss.scala | 12 +++++ app/templating/PaginatorHelper.scala | 22 --------- app/templating/UserHelper.scala | 5 -- app/views/base/bits.scala | 46 ++++++++++++++++++- app/views/forum/bits.scala | 18 -------- app/views/forum/categ.scala | 2 +- app/views/forum/topic.scala | 3 +- app/views/swiss/show.scala | 41 ++++++++++++++++- conf/routes | 1 + modules/swiss/src/main/Env.scala | 2 + modules/swiss/src/main/SwissPairing.scala | 1 + modules/swiss/src/main/SwissRoundPager.scala | 31 +++++++++++++ .../css/component}/_pagination.scss | 2 +- ui/site/css/build/_forum.scss | 1 + ui/site/css/forum/_forum.scss | 1 - ui/swiss/css/_round-pairings.scss | 10 ++++ ui/swiss/css/_show.scss | 1 + ui/swiss/css/_stats.scss | 7 ++- ui/swiss/css/build/_swiss.show.scss | 1 + ui/swiss/src/view/main.ts | 11 +++++ 20 files changed, 165 insertions(+), 53 deletions(-) create mode 100644 modules/swiss/src/main/SwissRoundPager.scala rename ui/{site/css/forum => common/css/component}/_pagination.scss (95%) create mode 100644 ui/swiss/css/_round-pairings.scss diff --git a/app/controllers/Swiss.scala b/app/controllers/Swiss.scala index 5f8f0fc507..7a8fc9ed9a 100644 --- a/app/controllers/Swiss.scala +++ b/app/controllers/Swiss.scala @@ -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 diff --git a/app/templating/PaginatorHelper.scala b/app/templating/PaginatorHelper.scala index 3708603fcc..cb6b73fc1e 100644 --- a/app/templating/PaginatorHelper.scala +++ b/app/templating/PaginatorHelper.scala @@ -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 - } -} diff --git a/app/templating/UserHelper.scala b/app/templating/UserHelper.scala index dcbc9d732e..0653c1f4d1 100644 --- a/app/templating/UserHelper.scala +++ b/app/templating/UserHelper.scala @@ -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) diff --git a/app/views/base/bits.scala b/app/views/base/bits.scala index 3d170b6756..e460a13e74 100644 --- a/app/views/base/bits.scala +++ b/app/views/base/bits.scala @@ -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(" … ") + 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 + } } diff --git a/app/views/forum/bits.scala b/app/views/forum/bits.scala index f51db512ee..0c64b0c2ba 100644 --- a/app/views/forum/bits.scala +++ b/app/views/forum/bits.scala @@ -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(" … ") - 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") } diff --git a/app/views/forum/categ.scala b/app/views/forum/categ.scala index c95de4d2ba..1ab64b49b0 100644 --- a/app/views/forum/categ.scala +++ b/app/views/forum/categ.scala @@ -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 ) diff --git a/app/views/forum/topic.scala b/app/views/forum/topic.scala index 0b254a490f..6771386dce 100644 --- a/app/views/forum/topic.scala +++ b/app/views/forum/topic.scala @@ -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( diff --git a/app/views/swiss/show.scala b/app/views/swiss/show.scala index 0316c9f6b4..0d4afede3d 100644 --- a/app/views/swiss/show.scala +++ b/app/views/swiss/show.scala @@ -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") + ) + } } diff --git a/conf/routes b/conf/routes index 762b298496..4c9767501c 100644 --- a/conf/routes +++ b/conf/routes @@ -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) diff --git a/modules/swiss/src/main/Env.scala b/modules/swiss/src/main/Env.scala index 61c6cbb21f..1b294cc787 100644 --- a/modules/swiss/src/main/Env.scala +++ b/modules/swiss/src/main/Env.scala @@ -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] diff --git a/modules/swiss/src/main/SwissPairing.scala b/modules/swiss/src/main/SwissPairing.scala index 28947e700b..8d0c4485ab 100644 --- a/modules/swiss/src/main/SwissPairing.scala +++ b/modules/swiss/src/main/SwissPairing.scala @@ -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 { diff --git a/modules/swiss/src/main/SwissRoundPager.scala b/modules/swiss/src/main/SwissRoundPager.scala new file mode 100644 index 0000000000..e909e740b9 --- /dev/null +++ b/modules/swiss/src/main/SwissRoundPager.scala @@ -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 + ) +} diff --git a/ui/site/css/forum/_pagination.scss b/ui/common/css/component/_pagination.scss similarity index 95% rename from ui/site/css/forum/_pagination.scss rename to ui/common/css/component/_pagination.scss index b42908cbe2..a2f0a3f6c5 100644 --- a/ui/site/css/forum/_pagination.scss +++ b/ui/common/css/component/_pagination.scss @@ -1,4 +1,4 @@ -.forum .pagination { +.pagination { color: $c-font-dimmer; display: flex; diff --git a/ui/site/css/build/_forum.scss b/ui/site/css/build/_forum.scss index 7bb7fcffac..d7a408befa 100644 --- a/ui/site/css/build/_forum.scss +++ b/ui/site/css/build/_forum.scss @@ -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'; diff --git a/ui/site/css/forum/_forum.scss b/ui/site/css/forum/_forum.scss index 4061754e98..801116e137 100644 --- a/ui/site/css/forum/_forum.scss +++ b/ui/site/css/forum/_forum.scss @@ -3,7 +3,6 @@ @import 'post'; @import 'completion'; @import 'table'; -@import 'pagination'; @import 'mention'; @import 'reactions'; diff --git a/ui/swiss/css/_round-pairings.scss b/ui/swiss/css/_round-pairings.scss new file mode 100644 index 0000000000..9f85172e15 --- /dev/null +++ b/ui/swiss/css/_round-pairings.scss @@ -0,0 +1,10 @@ +.pagination { + margin-left: var(--box-padding); + &--top { + padding-bottom: 1em; + border-bottom: $border; + } + &--bottom { + padding: 1em 0; + } +} diff --git a/ui/swiss/css/_show.scss b/ui/swiss/css/_show.scss index 220b2d8cf7..8bc7743fdc 100644 --- a/ui/swiss/css/_show.scss +++ b/ui/swiss/css/_show.scss @@ -11,6 +11,7 @@ $mq-col3: $mq-col3-uniboard; @import 'app-standing'; @import 'stats'; @import 'player-info'; +@import 'round-pairings'; .swiss { .pull-quote { diff --git a/ui/swiss/css/_stats.scss b/ui/swiss/css/_stats.scss index 3421354093..7dad30a985 100644 --- a/ui/swiss/css/_stats.scss +++ b/ui/swiss/css/_stats.scss @@ -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; + } } diff --git a/ui/swiss/css/build/_swiss.show.scss b/ui/swiss/css/build/_swiss.show.scss index bca676e21c..b3e828c47c 100644 --- a/ui/swiss/css/build/_swiss.show.scss +++ b/ui/swiss/css/build/_swiss.show.scss @@ -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'; diff --git a/ui/swiss/src/view/main.ts b/ui/swiss/src/view/main.ts index c7a3cebd6d..b4686a878c 100644 --- a/ui/swiss/src/view/main.ts +++ b/ui/swiss/src/view/main.ts @@ -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; }