add swiss tournament result to user activity feed - closes #7708

pull/8259/head
Thibault Duplessis 2021-02-25 11:43:29 +01:00
parent 9ea1775ddc
commit dc7c49f7b4
17 changed files with 128 additions and 21 deletions

View File

@ -8,6 +8,7 @@ import lila.api.Context
import lila.app.templating.Environment._ import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._ import lila.app.ui.ScalatagsTemplate._
import lila.user.User import lila.user.User
import lila.swiss.Swiss
object activity { object activity {
@ -33,6 +34,7 @@ object activity {
a.simuls map renderSimuls(u), a.simuls map renderSimuls(u),
a.studies map renderStudies, a.studies map renderStudies,
a.tours map renderTours, a.tours map renderTours,
a.swisses map renderSwisses,
a.teams map renderTeams, a.teams map renderTeams,
a.stream option renderStream(u), a.stream option renderStream(u),
a.signup option renderSignup a.signup option renderSignup
@ -245,7 +247,6 @@ object activity {
trans.activity.competedInNbTournaments.pluralSame(tours.nb), trans.activity.competedInNbTournaments.pluralSame(tours.nb),
subTag( subTag(
tours.best.map { t => tours.best.map { t =>
val link = a(href := routes.Tournament.show(t.tourId))(tournamentIdToName(t.tourId))
div( div(
cls := List( cls := List(
"is-gold" -> (t.rank == 1), "is-gold" -> (t.rank == 1),
@ -258,7 +259,32 @@ object activity {
strong(t.rank), strong(t.rank),
t.rankRatio.percent, t.rankRatio.percent,
t.nbGames, 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 br
) )

View File

@ -215,7 +215,7 @@ lazy val pool = module("pool",
) )
lazy val activity = module("activity", 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 reactivemongo.bundle
) )

View File

@ -5,6 +5,7 @@ import org.joda.time.Interval
import lila.common.Day import lila.common.Day
import lila.user.User import lila.user.User
import lila.swiss.Swiss
case class Activity( case class Activity(
id: Activity.Id, id: Activity.Id,
@ -20,6 +21,7 @@ case class Activity(
follows: Option[Follows] = None, follows: Option[Follows] = None,
studies: Option[Studies] = None, studies: Option[Studies] = None,
teams: Option[Teams] = None, teams: Option[Teams] = None,
swisses: Option[Swisses] = None,
stream: Boolean = false stream: Boolean = false
) { ) {
@ -40,7 +42,8 @@ case class Activity(
patron, patron,
follows, follows,
studies, studies,
teams teams,
swisses
) )
.forall(_.isEmpty) .forall(_.isEmpty)
} }

View File

@ -17,7 +17,8 @@ final class ActivityReadApi(
postApi: lila.forum.PostApi, postApi: lila.forum.PostApi,
simulApi: lila.simul.SimulApi, simulApi: lila.simul.SimulApi,
studyApi: lila.study.StudyApi, studyApi: lila.study.StudyApi,
tourLeaderApi: lila.tournament.LeaderboardApi tourLeaderApi: lila.tournament.LeaderboardApi,
swissApi: lila.swiss.SwissApi
)(implicit ec: scala.concurrent.ExecutionContext) { )(implicit ec: scala.concurrent.ExecutionContext) {
import BSONHandlers._ import BSONHandlers._
@ -83,13 +84,13 @@ final class ActivityReadApi(
.?? { simuls => .?? { simuls =>
simulApi byIds simuls.value.map(_.value) dmap some simulApi byIds simuls.value.map(_.value) dmap some
} }
.map(_ filter (_.nonEmpty)) .dmap(_.filter(_.nonEmpty))
studies <- studies <-
a.studies a.studies
.?? { studies => .?? { studies =>
studyApi publicIdNames studies.value dmap some studyApi publicIdNames studies.value dmap some
} }
.map(_ filter (_.nonEmpty)) .dmap(_.filter(_.nonEmpty))
tours <- a.games.exists(_.hasNonCorres) ?? { tours <- a.games.exists(_.hasNonCorres) ?? {
val dateRange = a.date -> a.date.plusDays(1) val dateRange = a.date -> a.date.plusDays(1)
tourLeaderApi tourLeaderApi
@ -106,6 +107,21 @@ final class ActivityReadApi(
} }
.mon(_.user segment "activity.tours") .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( } yield ActivityView(
interval = a.interval, interval = a.interval,
games = a.games, games = a.games,
@ -121,6 +137,7 @@ final class ActivityReadApi(
studies = studies, studies = studies,
teams = a.teams, teams = a.teams,
tours = tours, tours = tours,
swisses = swisses,
stream = a.stream stream = a.stream
) )

View File

@ -10,6 +10,7 @@ import lila.tournament.LeaderboardApi.{ Entry => TourEntry }
import activities._ import activities._
import model._ import model._
import lila.swiss.Swiss
case class ActivityView( case class ActivityView(
interval: Interval, interval: Interval,
@ -26,6 +27,7 @@ case class ActivityView(
studies: Option[List[Study.IdName]] = None, studies: Option[List[Study.IdName]] = None,
teams: Option[Teams] = None, teams: Option[Teams] = None,
tours: Option[ActivityView.Tours] = None, tours: Option[ActivityView.Tours] = None,
swisses: Option[List[(Swiss.IdName, Int)]] = None,
stream: Boolean = false, stream: Boolean = false,
signup: Boolean = false signup: Boolean = false
) )

View File

@ -150,6 +150,11 @@ final class ActivityWriteApi(
def streamStart(userId: User.ID) = def streamStart(userId: User.ID) =
update(userId) { _.copy(stream = true).some } 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)) def erase(user: User) = coll.delete.one(regexId(user.id))
private def simulParticipant(simul: lila.simul.Simul, userId: String) = private def simulParticipant(simul: lila.simul.Simul, userId: String) =

View File

@ -7,8 +7,10 @@ import lila.common.{ Day, Iso }
import lila.db.dsl._ import lila.db.dsl._
import lila.rating.BSONHandlers.perfTypeKeyIso import lila.rating.BSONHandlers.perfTypeKeyIso
import lila.rating.PerfType import lila.rating.PerfType
import lila.study.BSONHandlers._ import lila.study.BSONHandlers.StudyIdBSONHandler
import lila.study.Study import lila.study.Study
import lila.swiss.BsonHandlers.swissIdHandler
import lila.swiss.Swiss
import lila.user.User import lila.user.User
private object BSONHandlers { private object BSONHandlers {
@ -117,6 +119,13 @@ private object BSONHandlers {
implicit private lazy val teamsHandler = implicit private lazy val teamsHandler =
isoHandler[Teams, List[String]]((s: Teams) => s.value, Teams.apply _) 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 { object ActivityFields {
val id = "_id" val id = "_id"
val games = "g" val games = "g"
@ -131,6 +140,7 @@ private object BSONHandlers {
val follows = "f" val follows = "f"
val studies = "t" val studies = "t"
val teams = "e" val teams = "e"
val swisses = "w"
val stream = "st" val stream = "st"
} }
@ -153,6 +163,7 @@ private object BSONHandlers {
follows = r.getO[Follows](follows).filterNot(_.isEmpty), follows = r.getO[Follows](follows).filterNot(_.isEmpty),
studies = r.getO[Studies](studies), studies = r.getO[Studies](studies),
teams = r.getO[Teams](teams), teams = r.getO[Teams](teams),
swisses = r.getO[Swisses](swisses),
stream = r.getD[Boolean](stream) stream = r.getD[Boolean](stream)
) )
@ -171,6 +182,7 @@ private object BSONHandlers {
follows -> o.follows, follows -> o.follows,
studies -> o.studies, studies -> o.studies,
teams -> o.teams, teams -> o.teams,
swisses -> o.swisses,
stream -> o.stream.option(true) stream -> o.stream.option(true)
) )
} }

View File

@ -16,7 +16,8 @@ final class Env(
studyApi: lila.study.StudyApi, studyApi: lila.study.StudyApi,
tourLeaderApi: lila.tournament.LeaderboardApi, tourLeaderApi: lila.tournament.LeaderboardApi,
getTourName: lila.tournament.GetTourName, getTourName: lila.tournament.GetTourName,
getTeamName: lila.team.GetTeamName getTeamName: lila.team.GetTeamName,
swissApi: lila.swiss.SwissApi
)(implicit )(implicit
ec: scala.concurrent.ExecutionContext, ec: scala.concurrent.ExecutionContext,
system: ActorSystem system: ActorSystem
@ -52,7 +53,8 @@ final class Env(
"relation", "relation",
"startStudy", "startStudy",
"streamStart", "streamStart",
"gdprErase" "gdprErase",
"swissFinish"
) { ) {
case lila.forum.actorApi.CreatePost(post) => write.forumPost(post).unit case lila.forum.actorApi.CreatePost(post) => write.forumPost(post).unit
case prog: lila.practice.PracticeProgress.OnComplete => write.practice(prog).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.team.JoinTeam(id, userId) => write.team(id, userId).unit
case lila.hub.actorApi.streamer.StreamStart(userId) => write.streamStart(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.user.User.GDPRErase(user) => write.erase(user).unit
case lila.swiss.SwissFinish(swissId, ranking) => write.swiss(swissId, ranking)
} }
} }

View File

@ -1,11 +1,12 @@
package lila.activity package lila.activity
import model._
import ornicar.scalalib.Zero import ornicar.scalalib.Zero
import lila.rating.PerfType import lila.rating.PerfType
import lila.study.Study import lila.study.Study
import lila.swiss.Swiss
import lila.user.User import lila.user.User
import model._
object activities { object activities {
@ -103,4 +104,10 @@ object activities {
def +(s: String) = copy(value = (s :: value).distinct take maxSubEntries) def +(s: String) = copy(value = (s :: value).distinct take maxSubEntries)
} }
implicit val TeamsZero = Zero.instance(Teams(Nil)) 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))
} }

View File

@ -966,6 +966,8 @@ val `joinedNbSimuls` = new I18nKey("activity:joinedNbSimuls")
val `createdNbStudies` = new I18nKey("activity:createdNbStudies") val `createdNbStudies` = new I18nKey("activity:createdNbStudies")
val `competedInNbTournaments` = new I18nKey("activity:competedInNbTournaments") val `competedInNbTournaments` = new I18nKey("activity:competedInNbTournaments")
val `rankedInTournament` = new I18nKey("activity:rankedInTournament") val `rankedInTournament` = new I18nKey("activity:rankedInTournament")
val `competedInNbSwissTournaments` = new I18nKey("activity:competedInNbSwissTournaments")
val `rankedInSwissTournament` = new I18nKey("activity:rankedInSwissTournament")
val `joinedNbTeams` = new I18nKey("activity:joinedNbTeams") val `joinedNbTeams` = new I18nKey("activity:joinedNbTeams")
} }

View File

@ -89,7 +89,6 @@ object Study {
implicit val nameIso = lila.common.Iso.string[Name](Name.apply, _.value) implicit val nameIso = lila.common.Iso.string[Name](Name.apply, _.value)
case class IdName(_id: Id, name: Name) { case class IdName(_id: Id, name: Name) {
def id = _id def id = _id
} }

View File

@ -10,7 +10,7 @@ import lila.db.BSON
import lila.db.dsl._ import lila.db.dsl._
import lila.user.User import lila.user.User
private object BsonHandlers { object BsonHandlers {
implicit val variantHandler = variantByKeyHandler implicit val variantHandler = variantByKeyHandler
implicit val clockHandler = clockConfigHandler implicit val clockHandler = clockConfigHandler
@ -130,4 +130,7 @@ private object BsonHandlers {
"garbage" -> s.unrealisticSettings.option(true) "garbage" -> s.unrealisticSettings.option(true)
) )
} }
import Swiss.IdName
implicit val SwissIdNameBSONHandler = Macros.handler[IdName]
} }

View File

@ -1,6 +1,7 @@
package lila.swiss package lila.swiss
import chess.Clock.{ Config => ClockConfig } import chess.Clock.{ Config => ClockConfig }
import chess.format.FEN
import chess.Speed import chess.Speed
import org.joda.time.DateTime import org.joda.time.DateTime
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -8,7 +9,6 @@ import scala.concurrent.duration._
import lila.hub.LightTeam.TeamID import lila.hub.LightTeam.TeamID
import lila.rating.PerfType import lila.rating.PerfType
import lila.user.User import lila.user.User
import chess.format.FEN
case class Swiss( case class Swiss(
_id: Swiss.Id, _id: Swiss.Id,
@ -93,6 +93,10 @@ object Swiss {
case class Performance(value: Float) extends AnyVal case class Performance(value: Float) extends AnyVal
case class Score(value: Int) extends AnyVal case class Score(value: Int) extends AnyVal
case class IdName(_id: Id, name: String) {
def id = _id
}
case class Settings( case class Settings(
nbRounds: Int, nbRounds: Int,
rated: Boolean, rated: Boolean,

View File

@ -430,9 +430,9 @@ final class SwissApi(
private[swiss] def finish(oldSwiss: Swiss): Funit = private[swiss] def finish(oldSwiss: Swiss): Funit =
Sequencing(oldSwiss.id)(startedById) { swiss => Sequencing(oldSwiss.id)(startedById) { swiss =>
colls.pairing.countSel($doc(SwissPairing.Fields.swissId -> swiss.id)) flatMap { colls.pairing.exists($doc(SwissPairing.Fields.swissId -> swiss.id)) flatMap {
case 0 => destroy(swiss) if (_) doFinish(swiss)
case _ => doFinish(swiss) else destroy(swiss)
} }
} }
private def doFinish(swiss: Swiss): Funit = private def doFinish(swiss: Swiss): Funit =
@ -459,6 +459,15 @@ final class SwissApi(
} >>- { } >>- {
systemChat(swiss.id, s"Tournament completed!") systemChat(swiss.id, s"Tournament completed!")
socket.reload(swiss.id) 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 = { def kill(swiss: Swiss): Funit = {
@ -639,6 +648,11 @@ final class SwissApi(
.zipWithIndex .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]( private def Sequencing[A: Zero](
id: Swiss.Id id: Swiss.Id
)(fetch: Swiss.Id => Fu[Option[Swiss]])(run: Swiss => Fu[A]): Fu[A] = )(fetch: Swiss.Id => Fu[Option[Swiss]])(run: Swiss => Fu[A]): Fu[A] =

View File

@ -26,3 +26,5 @@ case class FeaturedSwisses(
created: List[Swiss], created: List[Swiss],
started: List[Swiss] started: List[Swiss]
) )
case class SwissFinish(id: Swiss.Id, ranking: Ranking)

View File

@ -4,9 +4,9 @@ import lila.user.User
package object swiss extends PackageObject { 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 // FIDE TRF player IDs
private[swiss] type PlayerIds = Map[User.ID, Int] private[swiss] type PlayerIds = Map[User.ID, Int]

View File

@ -55,13 +55,21 @@
<item quantity="other">Created %s new studies</item> <item quantity="other">Created %s new studies</item>
</plurals> </plurals>
<plurals name="competedInNbTournaments"> <plurals name="competedInNbTournaments">
<item quantity="one">Competed in %s tournament</item> <item quantity="one">Competed in %s Arena tournament</item>
<item quantity="other">Competed in %s tournaments</item> <item quantity="other">Competed in %s Arena tournaments</item>
</plurals> </plurals>
<plurals name="rankedInTournament"> <plurals name="rankedInTournament">
<item quantity="one">Ranked #%1$s (top %2$s%%) with %3$s game in %4$s</item> <item quantity="one">Ranked #%1$s (top %2$s%%) with %3$s game in %4$s</item>
<item quantity="other">Ranked #%1$s (top %2$s%%) with %3$s games in %4$s</item> <item quantity="other">Ranked #%1$s (top %2$s%%) with %3$s games in %4$s</item>
</plurals> </plurals>
<plurals name="competedInNbSwissTournaments">
<item quantity="one">Competed in %s swiss tournament</item>
<item quantity="other">Competed in %s swiss tournaments</item>
</plurals>
<plurals name="rankedInSwissTournament">
<item quantity="one">Ranked #%1$s in %2$s</item>
<item quantity="other">Ranked #%1$s in %2$s</item>
</plurals>
<string name="signedUp">Signed up to lichess.org</string> <string name="signedUp">Signed up to lichess.org</string>
<plurals name="joinedNbTeams"> <plurals name="joinedNbTeams">
<item quantity="one">Joined %s team</item> <item quantity="one">Joined %s team</item>