diff --git a/app/views/activity.scala b/app/views/activity.scala index 92e08f9594..b704a88899 100644 --- a/app/views/activity.scala +++ b/app/views/activity.scala @@ -8,6 +8,7 @@ import lila.api.Context import lila.app.templating.Environment._ import lila.app.ui.ScalatagsTemplate._ import lila.user.User +import lila.swiss.Swiss object activity { @@ -33,6 +34,7 @@ object activity { a.simuls map renderSimuls(u), a.studies map renderStudies, a.tours map renderTours, + a.swisses map renderSwisses, a.teams map renderTeams, a.stream option renderStream(u), a.signup option renderSignup @@ -245,7 +247,6 @@ object activity { trans.activity.competedInNbTournaments.pluralSame(tours.nb), subTag( tours.best.map { t => - val link = a(href := routes.Tournament.show(t.tourId))(tournamentIdToName(t.tourId)) div( cls := List( "is-gold" -> (t.rank == 1), @@ -258,7 +259,32 @@ object activity { strong(t.rank), t.rankRatio.percent, t.nbGames, - link + a(href := routes.Tournament.show(t.tourId))(tournamentIdToName(t.tourId)) + ), + br + ) + } + ) + ) + ) + + private def renderSwisses(swisses: List[(Swiss.IdName, Int)])(implicit ctx: Context) = + entryTag( + iconTag("g"), + div( + trans.activity.competedInNbSwissTournaments.pluralSame(swisses.size), + subTag( + swisses.map { case (swiss, rank) => + div( + cls := List( + "is-gold" -> (rank == 1), + "text" -> (rank <= 3) + ), + dataIcon := (rank <= 3).option("g") + )( + trans.activity.rankedInSwissTournament( + strong(rank), + a(href := routes.Swiss.show(swiss.id.value))(swiss.name) ), br ) diff --git a/build.sbt b/build.sbt index af47d4c514..b5ccb186b5 100644 --- a/build.sbt +++ b/build.sbt @@ -215,7 +215,7 @@ lazy val pool = module("pool", ) lazy val activity = module("activity", - Seq(common, game, analyse, user, forum, study, pool, puzzle, tournament, practice, team), + Seq(common, game, analyse, user, forum, study, pool, puzzle, tournament, swiss, practice, team), reactivemongo.bundle ) diff --git a/modules/activity/src/main/Activity.scala b/modules/activity/src/main/Activity.scala index f1aa8f4be5..2f7cef8265 100644 --- a/modules/activity/src/main/Activity.scala +++ b/modules/activity/src/main/Activity.scala @@ -5,6 +5,7 @@ import org.joda.time.Interval import lila.common.Day import lila.user.User +import lila.swiss.Swiss case class Activity( id: Activity.Id, @@ -20,6 +21,7 @@ case class Activity( follows: Option[Follows] = None, studies: Option[Studies] = None, teams: Option[Teams] = None, + swisses: Option[Swisses] = None, stream: Boolean = false ) { @@ -40,7 +42,8 @@ case class Activity( patron, follows, studies, - teams + teams, + swisses ) .forall(_.isEmpty) } diff --git a/modules/activity/src/main/ActivityReadApi.scala b/modules/activity/src/main/ActivityReadApi.scala index 26ce4804b9..6612e9213c 100644 --- a/modules/activity/src/main/ActivityReadApi.scala +++ b/modules/activity/src/main/ActivityReadApi.scala @@ -17,7 +17,8 @@ final class ActivityReadApi( postApi: lila.forum.PostApi, simulApi: lila.simul.SimulApi, studyApi: lila.study.StudyApi, - tourLeaderApi: lila.tournament.LeaderboardApi + tourLeaderApi: lila.tournament.LeaderboardApi, + swissApi: lila.swiss.SwissApi )(implicit ec: scala.concurrent.ExecutionContext) { import BSONHandlers._ @@ -83,13 +84,13 @@ final class ActivityReadApi( .?? { simuls => simulApi byIds simuls.value.map(_.value) dmap some } - .map(_ filter (_.nonEmpty)) + .dmap(_.filter(_.nonEmpty)) studies <- a.studies .?? { studies => studyApi publicIdNames studies.value dmap some } - .map(_ filter (_.nonEmpty)) + .dmap(_.filter(_.nonEmpty)) tours <- a.games.exists(_.hasNonCorres) ?? { val dateRange = a.date -> a.date.plusDays(1) tourLeaderApi @@ -106,6 +107,21 @@ final class ActivityReadApi( } .mon(_.user segment "activity.tours") } + swisses <- + a.swisses + .?? { swisses => + swissApi + .idNames(swisses.value.map(_.id)) + .map { + _.flatMap { idName => + swisses.value.find(_.id == idName.id) map { s => + (idName, s.rank) + } + } + } + .dmap(_.some.filter(_.nonEmpty)) + } + } yield ActivityView( interval = a.interval, games = a.games, @@ -121,6 +137,7 @@ final class ActivityReadApi( studies = studies, teams = a.teams, tours = tours, + swisses = swisses, stream = a.stream ) diff --git a/modules/activity/src/main/ActivityView.scala b/modules/activity/src/main/ActivityView.scala index f509c7e3d4..823334a10e 100644 --- a/modules/activity/src/main/ActivityView.scala +++ b/modules/activity/src/main/ActivityView.scala @@ -10,6 +10,7 @@ import lila.tournament.LeaderboardApi.{ Entry => TourEntry } import activities._ import model._ +import lila.swiss.Swiss case class ActivityView( interval: Interval, @@ -26,6 +27,7 @@ case class ActivityView( studies: Option[List[Study.IdName]] = None, teams: Option[Teams] = None, tours: Option[ActivityView.Tours] = None, + swisses: Option[List[(Swiss.IdName, Int)]] = None, stream: Boolean = false, signup: Boolean = false ) diff --git a/modules/activity/src/main/ActivityWriteApi.scala b/modules/activity/src/main/ActivityWriteApi.scala index 3663657414..ffec658a26 100644 --- a/modules/activity/src/main/ActivityWriteApi.scala +++ b/modules/activity/src/main/ActivityWriteApi.scala @@ -150,6 +150,11 @@ final class ActivityWriteApi( def streamStart(userId: User.ID) = update(userId) { _.copy(stream = true).some } + def swiss(id: lila.swiss.Swiss.Id, ranking: lila.swiss.Ranking) = + ranking.map { case (userId, rank) => + update(userId) { a => a.copy(swisses = Some(~a.swisses + SwissRank(id, rank))).some } + }.sequenceFu + def erase(user: User) = coll.delete.one(regexId(user.id)) private def simulParticipant(simul: lila.simul.Simul, userId: String) = diff --git a/modules/activity/src/main/BSONHandlers.scala b/modules/activity/src/main/BSONHandlers.scala index a6b885f9c0..f49297808d 100644 --- a/modules/activity/src/main/BSONHandlers.scala +++ b/modules/activity/src/main/BSONHandlers.scala @@ -7,8 +7,10 @@ import lila.common.{ Day, Iso } import lila.db.dsl._ import lila.rating.BSONHandlers.perfTypeKeyIso import lila.rating.PerfType -import lila.study.BSONHandlers._ +import lila.study.BSONHandlers.StudyIdBSONHandler import lila.study.Study +import lila.swiss.BsonHandlers.swissIdHandler +import lila.swiss.Swiss import lila.user.User private object BSONHandlers { @@ -117,6 +119,13 @@ private object BSONHandlers { implicit private lazy val teamsHandler = isoHandler[Teams, List[String]]((s: Teams) => s.value, Teams.apply _) + implicit lazy val swissRankHandler = new lila.db.BSON[SwissRank] { + def reads(r: lila.db.BSON.Reader) = SwissRank(Swiss.Id(r.str("i")), r.intD("r")) + def writes(w: lila.db.BSON.Writer, s: SwissRank) = BSONDocument("i" -> s.id, "r" -> s.rank) + } + implicit private lazy val swissesHandler = + isoHandler[Swisses, List[SwissRank]]((s: Swisses) => s.value, Swisses.apply _) + object ActivityFields { val id = "_id" val games = "g" @@ -131,6 +140,7 @@ private object BSONHandlers { val follows = "f" val studies = "t" val teams = "e" + val swisses = "w" val stream = "st" } @@ -153,6 +163,7 @@ private object BSONHandlers { follows = r.getO[Follows](follows).filterNot(_.isEmpty), studies = r.getO[Studies](studies), teams = r.getO[Teams](teams), + swisses = r.getO[Swisses](swisses), stream = r.getD[Boolean](stream) ) @@ -171,6 +182,7 @@ private object BSONHandlers { follows -> o.follows, studies -> o.studies, teams -> o.teams, + swisses -> o.swisses, stream -> o.stream.option(true) ) } diff --git a/modules/activity/src/main/Env.scala b/modules/activity/src/main/Env.scala index ca26ff595a..4f785c1394 100644 --- a/modules/activity/src/main/Env.scala +++ b/modules/activity/src/main/Env.scala @@ -16,7 +16,8 @@ final class Env( studyApi: lila.study.StudyApi, tourLeaderApi: lila.tournament.LeaderboardApi, getTourName: lila.tournament.GetTourName, - getTeamName: lila.team.GetTeamName + getTeamName: lila.team.GetTeamName, + swissApi: lila.swiss.SwissApi )(implicit ec: scala.concurrent.ExecutionContext, system: ActorSystem @@ -52,7 +53,8 @@ final class Env( "relation", "startStudy", "streamStart", - "gdprErase" + "gdprErase", + "swissFinish" ) { case lila.forum.actorApi.CreatePost(post) => write.forumPost(post).unit case prog: lila.practice.PracticeProgress.OnComplete => write.practice(prog).unit @@ -67,5 +69,6 @@ final class Env( case lila.hub.actorApi.team.JoinTeam(id, userId) => write.team(id, userId).unit case lila.hub.actorApi.streamer.StreamStart(userId) => write.streamStart(userId).unit case lila.user.User.GDPRErase(user) => write.erase(user).unit + case lila.swiss.SwissFinish(swissId, ranking) => write.swiss(swissId, ranking) } } diff --git a/modules/activity/src/main/activities.scala b/modules/activity/src/main/activities.scala index 45fef03e2e..3e19954002 100644 --- a/modules/activity/src/main/activities.scala +++ b/modules/activity/src/main/activities.scala @@ -1,11 +1,12 @@ package lila.activity +import model._ import ornicar.scalalib.Zero import lila.rating.PerfType import lila.study.Study +import lila.swiss.Swiss import lila.user.User -import model._ object activities { @@ -103,4 +104,10 @@ object activities { def +(s: String) = copy(value = (s :: value).distinct take maxSubEntries) } implicit val TeamsZero = Zero.instance(Teams(Nil)) + + case class SwissRank(id: Swiss.Id, rank: Int) + case class Swisses(value: List[SwissRank]) extends AnyVal { + def +(s: SwissRank) = copy(value = (s :: value) take maxSubEntries) + } + implicit val SwissesZero = Zero.instance(Swisses(Nil)) } diff --git a/modules/i18n/src/main/I18nKeys.scala b/modules/i18n/src/main/I18nKeys.scala index f35773fe7e..21684adf77 100644 --- a/modules/i18n/src/main/I18nKeys.scala +++ b/modules/i18n/src/main/I18nKeys.scala @@ -966,6 +966,8 @@ val `joinedNbSimuls` = new I18nKey("activity:joinedNbSimuls") val `createdNbStudies` = new I18nKey("activity:createdNbStudies") val `competedInNbTournaments` = new I18nKey("activity:competedInNbTournaments") val `rankedInTournament` = new I18nKey("activity:rankedInTournament") +val `competedInNbSwissTournaments` = new I18nKey("activity:competedInNbSwissTournaments") +val `rankedInSwissTournament` = new I18nKey("activity:rankedInSwissTournament") val `joinedNbTeams` = new I18nKey("activity:joinedNbTeams") } diff --git a/modules/study/src/main/Study.scala b/modules/study/src/main/Study.scala index ea7145a863..c461d88f68 100644 --- a/modules/study/src/main/Study.scala +++ b/modules/study/src/main/Study.scala @@ -89,7 +89,6 @@ object Study { implicit val nameIso = lila.common.Iso.string[Name](Name.apply, _.value) case class IdName(_id: Id, name: Name) { - def id = _id } diff --git a/modules/swiss/src/main/BsonHandlers.scala b/modules/swiss/src/main/BsonHandlers.scala index 8558fad96c..6234a9f43e 100644 --- a/modules/swiss/src/main/BsonHandlers.scala +++ b/modules/swiss/src/main/BsonHandlers.scala @@ -10,7 +10,7 @@ import lila.db.BSON import lila.db.dsl._ import lila.user.User -private object BsonHandlers { +object BsonHandlers { implicit val variantHandler = variantByKeyHandler implicit val clockHandler = clockConfigHandler @@ -130,4 +130,7 @@ private object BsonHandlers { "garbage" -> s.unrealisticSettings.option(true) ) } + + import Swiss.IdName + implicit val SwissIdNameBSONHandler = Macros.handler[IdName] } diff --git a/modules/swiss/src/main/Swiss.scala b/modules/swiss/src/main/Swiss.scala index da3693bfd3..2557246831 100644 --- a/modules/swiss/src/main/Swiss.scala +++ b/modules/swiss/src/main/Swiss.scala @@ -1,6 +1,7 @@ package lila.swiss import chess.Clock.{ Config => ClockConfig } +import chess.format.FEN import chess.Speed import org.joda.time.DateTime import scala.concurrent.duration._ @@ -8,7 +9,6 @@ import scala.concurrent.duration._ import lila.hub.LightTeam.TeamID import lila.rating.PerfType import lila.user.User -import chess.format.FEN case class Swiss( _id: Swiss.Id, @@ -93,6 +93,10 @@ object Swiss { case class Performance(value: Float) extends AnyVal case class Score(value: Int) extends AnyVal + case class IdName(_id: Id, name: String) { + def id = _id + } + case class Settings( nbRounds: Int, rated: Boolean, diff --git a/modules/swiss/src/main/SwissApi.scala b/modules/swiss/src/main/SwissApi.scala index e066390a5f..4596514691 100644 --- a/modules/swiss/src/main/SwissApi.scala +++ b/modules/swiss/src/main/SwissApi.scala @@ -430,9 +430,9 @@ final class SwissApi( private[swiss] def finish(oldSwiss: Swiss): Funit = Sequencing(oldSwiss.id)(startedById) { swiss => - colls.pairing.countSel($doc(SwissPairing.Fields.swissId -> swiss.id)) flatMap { - case 0 => destroy(swiss) - case _ => doFinish(swiss) + colls.pairing.exists($doc(SwissPairing.Fields.swissId -> swiss.id)) flatMap { + if (_) doFinish(swiss) + else destroy(swiss) } } private def doFinish(swiss: Swiss): Funit = @@ -459,6 +459,15 @@ final class SwissApi( } >>- { systemChat(swiss.id, s"Tournament completed!") socket.reload(swiss.id) + system.scheduler + .scheduleOnce(10 seconds) { + // we're delaying this to make sure the ranking has been recomputed + // since doFinish is called by finishGame before that + rankingApi(swiss) foreach { ranking => + Bus.publish(SwissFinish(swiss.id, ranking), "swissFinish") + } + } + .unit } def kill(swiss: Swiss): Funit = { @@ -639,6 +648,11 @@ final class SwissApi( .zipWithIndex } + private val idNameProjection = $doc("name" -> true) + + def idNames(ids: List[Swiss.Id]): Fu[List[Swiss.IdName]] = + colls.swiss.find($inIds(ids), idNameProjection.some).cursor[Swiss.IdName]().list() + private def Sequencing[A: Zero]( id: Swiss.Id )(fetch: Swiss.Id => Fu[Option[Swiss]])(run: Swiss => Fu[A]): Fu[A] = diff --git a/modules/swiss/src/main/model.scala b/modules/swiss/src/main/model.scala index 69747e0930..7d71b0bfdd 100644 --- a/modules/swiss/src/main/model.scala +++ b/modules/swiss/src/main/model.scala @@ -26,3 +26,5 @@ case class FeaturedSwisses( created: List[Swiss], started: List[Swiss] ) + +case class SwissFinish(id: Swiss.Id, ranking: Ranking) diff --git a/modules/swiss/src/main/package.scala b/modules/swiss/src/main/package.scala index fd977a58fd..b8fe8c3447 100644 --- a/modules/swiss/src/main/package.scala +++ b/modules/swiss/src/main/package.scala @@ -4,9 +4,9 @@ import lila.user.User package object swiss extends PackageObject { - private[swiss] val logger = lila.log("swiss") + type Ranking = Map[lila.user.User.ID, Int] - private[swiss] type Ranking = Map[lila.user.User.ID, Int] + private[swiss] val logger = lila.log("swiss") // FIDE TRF player IDs private[swiss] type PlayerIds = Map[User.ID, Int] diff --git a/translation/source/activity.xml b/translation/source/activity.xml index 20dc778168..d1ce05ae80 100644 --- a/translation/source/activity.xml +++ b/translation/source/activity.xml @@ -55,13 +55,21 @@ Created %s new studies - Competed in %s tournament - Competed in %s tournaments + Competed in %s Arena tournament + Competed in %s Arena tournaments Ranked #%1$s (top %2$s%%) with %3$s game in %4$s Ranked #%1$s (top %2$s%%) with %3$s games in %4$s + + Competed in %s swiss tournament + Competed in %s swiss tournaments + + + Ranked #%1$s in %2$s + Ranked #%1$s in %2$s + Signed up to lichess.org Joined %s team