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.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
)

View File

@ -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
)

View File

@ -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)
}

View File

@ -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
)

View File

@ -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
)

View File

@ -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) =

View File

@ -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)
)
}

View File

@ -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)
}
}

View File

@ -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))
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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]
}

View File

@ -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,

View File

@ -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] =

View File

@ -26,3 +26,5 @@ case class FeaturedSwisses(
created: 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 {
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]

View File

@ -55,13 +55,21 @@
<item quantity="other">Created %s new studies</item>
</plurals>
<plurals name="competedInNbTournaments">
<item quantity="one">Competed in %s tournament</item>
<item quantity="other">Competed in %s tournaments</item>
<item quantity="one">Competed in %s Arena tournament</item>
<item quantity="other">Competed in %s Arena tournaments</item>
</plurals>
<plurals name="rankedInTournament">
<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>
</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>
<plurals name="joinedNbTeams">
<item quantity="one">Joined %s team</item>