Skip to content

Commit

Permalink
Merge branch 'master' into show-opponent-rating-pref
Browse files Browse the repository at this point in the history
  • Loading branch information
ornicar authored Dec 18, 2024
2 parents 3ae2128 + 2ade9c1 commit 5640e11
Show file tree
Hide file tree
Showing 25 changed files with 189 additions and 107 deletions.
18 changes: 18 additions & 0 deletions app/controllers/Clas.scala
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,24 @@ final class Clas(env: Env, authC: Auth) extends LilaController(env):
else redirectTo(clas)
}

def studentMove(id: ClasId, username: UserStr) = Secure(_.Teacher) { ctx ?=> me ?=>
WithClassAndStudents(id): (clas, students) =>
WithStudent(clas, username): s =>
for
classes <- env.clas.api.clas.of(me)
others = classes.filter(_.id != clas.id)
res <- Ok.page(views.clas.student.move(clas, students, s, others))
yield res
}

def studentMovePost(id: ClasId, username: UserStr, to: ClasId) = SecureBody(_.Teacher) { ctx ?=> me ?=>
WithClassAndStudents(id): (clas, students) =>
WithStudent(clas, username): s =>
WithClass(to): toClas =>
for _ <- env.clas.api.student.move(s, toClas)
yield Redirect(routes.Clas.show(clas.id)).flashSuccess
}

def becomeTeacher = AuthBody { ctx ?=> me ?=>
couldBeTeacher.elseNotFound:
val perm = lila.core.perm.Permission.Teacher.dbKey
Expand Down
7 changes: 4 additions & 3 deletions app/controllers/Puzzle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -535,12 +535,13 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env):
Auth { ctx ?=> me ?=>
meOrFetch(username)
.flatMapz: user =>
(fuccess(isGranted(_.CheatHunter)) >>|
(fuccess(user.is(me) || isGranted(_.CheatHunter)) >>|
user.enabled.yes.so(env.clas.api.clas.isTeacherOf(me, user.id))).map {
_.option(user)
}
.dmap(_ | me.value)
.flatMap(f(_))
.flatMap:
case Some(user) => f(user)
case None => Redirect(routes.Puzzle.dashboard(Days(30), "home", none))
}

def WithPuzzlePerf[A](f: Perf ?=> Fu[A])(using Option[Me]): Fu[A] =
Expand Down
33 changes: 18 additions & 15 deletions app/controllers/User.scala
Original file line number Diff line number Diff line change
Expand Up @@ -553,21 +553,24 @@ final class User(
}

def perfStat(username: UserStr, perfKey: PerfKey) = Open:
Found(env.perfStat.api.data(username, perfKey)): data =>
negotiate(
Ok.async:
env.history
.ratingChartApi(data.user.user)
.map:
views.user.perfStatPage(data, _)
,
JsonOk:
getBool("graph")
.soFu:
env.history.ratingChartApi.singlePerf(data.user.user, data.stat.perfType.key)
.map: graph =>
env.perfStat.jsonView(data).add("graph", graph)
)
PerfType
.isLeaderboardable(perfKey)
.so:
Found(env.perfStat.api.data(username, perfKey)): data =>
negotiate(
Ok.async:
env.history
.ratingChartApi(data.user.user)
.map:
views.user.perfStatPage(data, _)
,
JsonOk:
getBool("graph")
.soFu:
env.history.ratingChartApi.singlePerf(data.user.user, data.stat.perfType.key)
.map: graph =>
env.perfStat.jsonView(data).add("graph", graph)
)

def autocomplete = OpenOrScoped(): ctx ?=>
NoTor:
Expand Down
2 changes: 1 addition & 1 deletion app/views/clas.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ object student:
lazy val formUi = lila.clas.ui.StudentFormUi(helpers, views.clas.ui, ui)

export ui.{ invite }
export formUi.{ newStudent as form, many as manyForm, edit, release, close }
export formUi.{ newStudent as form, many as manyForm, edit, release, close, move }

def show(
clas: Clas,
Expand Down
11 changes: 6 additions & 5 deletions bin/mongodb/recap-notif.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,17 @@ function sendToRandomOfflinePlayers() {
print(`+ ${countSent - lastPrinted} = ${countSent} / ${countAll} | ${user.createdAt.toLocaleDateString('fr')}`);
lastPrinted = countSent;
}
sleep(15 * newUsers.length);
sleep(10 * newUsers.length);
}
}
db.user4.find({
enabled: true,
createdAt: { $lt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7) },
createdAt: { $lt: new Date(year, 9, 1) },
seenAt: {
$gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 14),
$lt: new Date(Date.now() - 1000 * 60 * 20) // avoid the lila notif cache!
}
$gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30 * 3),
// $lt: new Date(Date.now() - 1000 * 60 * 20) // avoid the lila notif cache!
},
marks: { $nin: ['boost', 'engine', 'troll'] }
}).forEach(process);
process(); // flush the generator
}
Expand Down
2 changes: 2 additions & 0 deletions conf/clas.routes
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ POST /class/$id<\w{8}>/student/:username/release controllers.clas.Clas.studentR
GET /class/$id<\w{8}>/student/:username/close controllers.clas.Clas.studentClose(id: ClasId, username: UserStr)
POST /class/$id<\w{8}>/student/:username/close controllers.clas.Clas.studentClosePost(id: ClasId, username: UserStr)
POST /class/$id<\w{8}>/invitation/revoke controllers.clas.Clas.invitationRevoke(id: ClasInviteId)
GET /class/$id<\w{8}>/student/:username/move controllers.clas.Clas.studentMove(id: ClasId, username: UserStr)
POST /class/$id<\w{8}>/student/:username/move/$to<\w{8}> controllers.clas.Clas.studentMovePost(id: ClasId, username: UserStr, to: ClasId)
11 changes: 11 additions & 0 deletions modules/clas/src/main/ClasApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,17 @@ final class ClasApi(
sendWelcomeMessage(teacher.id, user, clas)).inject(Student.WithPassword(student, password))
}

def move(s: Student.WithUser, toClas: Clas)(using teacher: Me): Fu[Option[Student]] = for
_ <- closeAccount(s)
stu = s.student.copy(id = Student.makeId(s.user.id, toClas.id), clasId = toClas.id)
moved <- colls.student.insert
.one(stu)
.inject(stu.some)
.recoverWith(lila.db.recoverDuplicateKey { _ =>
student.get(toClas, s.user.id)
})
yield moved

def manyCreate(
clas: Clas,
data: ClasForm.ManyNewStudent,
Expand Down
2 changes: 1 addition & 1 deletion modules/clas/src/main/ClasMatesCache.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ final class ClasMatesCache(colls: ClasColls, cacheApi: CacheApi, studentCache: C
def get(studentId: UserId): Fu[Set[UserId]] =
studentCache.isStudent(studentId).so(cache.get(studentId))

private val cache = cacheApi[UserId, Set[UserId]](256, "clas.mates"):
private val cache = cacheApi[UserId, Set[UserId]](64, "clas.mates"):
_.expireAfterWrite(5 minutes)
.buildAsyncFuture(fetchMatesAndTeachers)

Expand Down
6 changes: 4 additions & 2 deletions modules/clas/src/main/ui/DashboardUi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,13 @@ final class DashboardUi(helpers: Helpers, ui: ClasUi)(using NetDomain):
tr(
td(userIdLink(i.userId.some)),
td(i.realName),
td(if i.accepted.has(false) then "Declined" else "Pending"),
td(
if i.accepted.has(false) then trans.clas.declined.txt() else trans.clas.pending.txt()
),
td(momentFromNow(i.created.at)),
td:
postForm(action := routes.Clas.invitationRevoke(i.id)):
submitButton(cls := "button button-red button-empty")("Revoke")
submitButton(cls := "button button-red button-empty")(trans.site.delete())
)
)
val archivedBox =
Expand Down
32 changes: 31 additions & 1 deletion modules/clas/src/main/ui/StudentFormUi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,11 @@ final class StudentFormUi(helpers: Helpers, clasUi: ClasUi, studentUi: StudentUi
cls := "button button-empty button-red",
title := trans.clas.closeDesc1.txt()
)(trans.clas.closeStudent())
)
),
a(
href := routes.Clas.studentMove(clas.id, s.user.username),
cls := "button button-empty"
)(trans.clas.moveToAnotherClass())
)
)
)
Expand Down Expand Up @@ -250,6 +254,32 @@ final class StudentFormUi(helpers: Helpers, clasUi: ClasUi, studentUi: StudentUi
)
)

def move(clas: Clas, students: List[Student], s: Student.WithUser, otherClasses: List[Clas])(using
Context
) =
ClasPage(s.user.username.value, Left(clas.withStudents(students)), s.student.some)(
cls := "student-show student-edit"
):

val classForms: Frag = otherClasses.map: toClass =>
postForm(action := routes.Clas.studentMovePost(clas.id, s.student.userId, toClass.id))(
form3.submit(toClass.name, icon = Icon.InternalArrow.some)(
cls := "yes-no-confirm button-blue button-empty",
title := trans.clas.moveToClass.txt(toClass.name)
)
)

frag(
studentUi.top(clas, s),
div(cls := "box__pad")(
h2(trans.clas.moveToAnotherClass()),
classForms,
form3.actions(
a(href := routes.Clas.studentShow(clas.id, s.user.username))(trans.site.cancel())
)
)
)

def close(clas: Clas, students: List[Student], s: Student.WithUser)(using Context) =
ClasPage(s.user.username.value, Left(clas.withStudents(students)), s.student.some)(
cls := "student-show student-edit"
Expand Down
6 changes: 4 additions & 2 deletions modules/coreI18n/src/main/key.scala
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@ object I18nKey:
val `welcomeToClass`: I18nKey = "class:welcomeToClass"
val `invitationToClass`: I18nKey = "class:invitationToClass"
val `clickToViewInvitation`: I18nKey = "class:clickToViewInvitation"
val `pending`: I18nKey = "class:pending"
val `declined`: I18nKey = "class:declined"
val `onlyVisibleToTeachers`: I18nKey = "class:onlyVisibleToTeachers"
val `lastActiveDate`: I18nKey = "class:lastActiveDate"
val `managed`: I18nKey = "class:managed"
Expand Down Expand Up @@ -330,6 +332,8 @@ object I18nKey:
val `anInvitationHasBeenSentToX`: I18nKey = "class:anInvitationHasBeenSentToX"
val `xAlreadyHasAPendingInvitation`: I18nKey = "class:xAlreadyHasAPendingInvitation"
val `xIsAKidAccountWarning`: I18nKey = "class:xIsAKidAccountWarning"
val `moveToClass`: I18nKey = "class:moveToClass"
val `moveToAnotherClass`: I18nKey = "class:moveToAnotherClass"
val `nbPendingInvitations`: I18nKey = "class:nbPendingInvitations"
val `nbTeachers`: I18nKey = "class:nbTeachers"
val `nbStudents`: I18nKey = "class:nbStudents"
Expand Down Expand Up @@ -659,8 +663,6 @@ object I18nKey:
val `ultraBulletBulletBlitzRapidClassicalAndCorrespondenceChess`: I18nKey = "features:ultraBulletBulletBlitzRapidClassicalAndCorrespondenceChess"
val `allFeaturesToCome`: I18nKey = "features:allFeaturesToCome"
val `landscapeSupportOnApp`: I18nKey = "features:landscapeSupportOnApp"
val `supportLichess`: I18nKey = "features:supportLichess"
val `contributeToLichessAndGetIcon`: I18nKey = "features:contributeToLichessAndGetIcon"
val `everybodyGetsAllFeaturesForFree`: I18nKey = "features:everybodyGetsAllFeaturesForFree"
val `weBelieveEveryChessPlayerDeservesTheBest`: I18nKey = "features:weBelieveEveryChessPlayerDeservesTheBest"
val `allFeaturesAreFreeForEverybody`: I18nKey = "features:allFeaturesAreFreeForEverybody"
Expand Down
9 changes: 5 additions & 4 deletions modules/perfStat/src/main/PerfStat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import java.time.Duration
import scalalib.HeapSort

import lila.rating.PerfType
import lila.rating.PerfType.GamePerf

extension (p: Pov) def loss = p.game.winner.map(_.color != p.color)

Expand Down Expand Up @@ -41,13 +42,13 @@ object PerfStat:

type Getter = (User, PerfType) => Fu[PerfStat]

def makeId(userId: UserId, perfType: PerfType) = s"$userId/${perfType.id}"
def makeId(userId: UserId, perf: GamePerf) = s"$userId/${perf.id}"

def init(userId: UserId, perfType: PerfType) =
def init(userId: UserId, perf: GamePerf) =
PerfStat(
id = makeId(userId, perfType),
id = makeId(userId, perf),
userId = userId,
perfType = perfType,
perfType = perf,
highest = none,
lowest = none,
bestWins = Results(Nil),
Expand Down
53 changes: 30 additions & 23 deletions modules/perfStat/src/main/PerfStatApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import lila.core.perm.Granter
import lila.rating.Glicko.minRating
import lila.rating.PerfExt.established
import lila.rating.{ PerfType, UserRankMap }
import lila.rating.PerfType.GamePerf

case class PerfStatData(
user: UserWithPerfs,
Expand All @@ -30,37 +31,43 @@ final class PerfStatApi(
extends lila.core.perf.PerfStatApi:

def data(name: UserStr, perfKey: PerfKey)(using me: Option[Me]): Fu[Option[PerfStatData]] =
userApi.withPerfs(name.id).flatMap {
_.filter: u =>
(u.enabled.yes && (!u.lame || me.exists(_.is(u.user)))) || me.soUse(Granter(_.UserModView))
.filter: u =>
!u.isBot || (perfKey != PerfKey.ultraBullet)
.soFu: u =>
for
oldPerfStat <- get(u.user.id, perfKey)
perfStat = oldPerfStat.copy(playStreak = oldPerfStat.playStreak.checkCurrent)
distribution <- u
.perfs(perfKey)
.established
.soFu(weeklyRatingDistribution(perfKey))
percentile = calcPercentile(distribution, u.perfs(perfKey).intRating)
percentileLow = perfStat.lowest.flatMap { r => calcPercentile(distribution, r.int) }
percentileHigh = perfStat.highest.flatMap { r => calcPercentile(distribution, r.int) }
_ = lightUserApi.preloadUser(u.user)
_ <- lightUserApi.preloadMany(perfStat.userIds)
yield PerfStatData(u, perfStat, rankingsOf(u.id), percentile, percentileLow, percentileHigh)
}
PerfType(perfKey) match
case pk: GamePerf =>
userApi.withPerfs(name.id).flatMap {
_.filter: u =>
(u.enabled.yes && (!u.lame || me.exists(_.is(u.user)))) || me.soUse(Granter(_.UserModView))
.filter: u =>
!u.isBot || (perfKey != PerfKey.ultraBullet)
.soFu: u =>
for
oldPerfStat <- get(u.user.id, pk)
perfStat = oldPerfStat.copy(playStreak = oldPerfStat.playStreak.checkCurrent)
distribution <- u
.perfs(perfKey)
.established
.soFu(weeklyRatingDistribution(perfKey))
percentile = calcPercentile(distribution, u.perfs(perfKey).intRating)
percentileLow = perfStat.lowest.flatMap { r => calcPercentile(distribution, r.int) }
percentileHigh = perfStat.highest.flatMap { r => calcPercentile(distribution, r.int) }
_ = lightUserApi.preloadUser(u.user)
_ <- lightUserApi.preloadMany(perfStat.userIds)
yield PerfStatData(u, perfStat, rankingsOf(u.id), percentile, percentileLow, percentileHigh)
}
case pk => fuccess(none)

private def calcPercentile(wrd: Option[List[Int]], intRating: IntRating): Option[Double] =
wrd.map: distrib =>
val (under, sum) = percentileOf(distrib, intRating)
Math.round(under * 1000.0 / sum) / 10.0

def get(user: UserId, perfType: PerfType): Fu[PerfStat] =
storage.find(user, perfType).getOrElse(indexer.userPerf(user, perfType))
def get(user: UserId, perf: GamePerf): Fu[PerfStat] =
storage.find(user, perf).getOrElse(indexer.userPerf(user, perf))

def highestRating(user: UserId, perfKey: PerfKey): Fu[Option[IntRating]] =
get(user, perfKey).map(_.highest.map(_.int))
PerfType
.gamePerf(perfKey)
.so: (gp: GamePerf) =>
get(user, gp).map(_.highest.map(_.int))

object weeklyRatingDistribution:

Expand Down
16 changes: 11 additions & 5 deletions modules/perfStat/src/main/PerfStatIndexer.scala
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
package lila.perfStat

import lila.rating.PerfType
import lila.rating.PerfType.GamePerf

final class PerfStatIndexer(
gameRepo: lila.core.game.GameRepo,
storage: PerfStatStorage
)(using Executor, Scheduler):

import PerfType.{ isLeaderboardable as isRelevant }

private val workQueue = scalalib.actor.AsyncActorSequencer(
maxSize = Max(64),
timeout = 10 seconds,
name = "perfStatIndexer",
lila.log.asyncActorMonitor.full
)

private[perfStat] def userPerf(user: UserId, perfKey: PerfKey): Fu[PerfStat] =
private[perfStat] def userPerf(user: UserId, perfKey: GamePerf): Fu[PerfStat] =
workQueue:
storage
.find(user, perfKey)
Expand All @@ -36,7 +39,10 @@ final class PerfStatIndexer(
addPov(Pov(game, player), userId)

private def addPov(pov: Pov, userId: UserId): Funit =
storage
.find(userId, pov.game.perfKey)
.flatMapz: perfStat =>
storage.update(perfStat, perfStat.agg(pov))
PerfType
.gamePerf(pov.game.perfKey)
.so: (pk: GamePerf) =>
storage
.find(userId, pk)
.flatMapz: perfStat =>
storage.update(perfStat, perfStat.agg(pov))
5 changes: 3 additions & 2 deletions modules/perfStat/src/main/PerfStatStorage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import lila.db.AsyncCollFailingSilently
import lila.db.dsl.{ *, given }
import lila.rating.BSONHandlers.perfTypeIdHandler
import lila.rating.PerfType
import lila.rating.PerfType.GamePerf

final class PerfStatStorage(coll: AsyncCollFailingSilently)(using Executor):

Expand All @@ -21,8 +22,8 @@ final class PerfStatStorage(coll: AsyncCollFailingSilently)(using Executor):
private given BSONDocumentHandler[Count] = Macros.handler
private given BSONDocumentHandler[PerfStat] = Macros.handler

def find(userId: UserId, perfType: PerfType): Fu[Option[PerfStat]] =
coll(_.byId[PerfStat](PerfStat.makeId(userId, perfType)))
def find(userId: UserId, perf: GamePerf): Fu[Option[PerfStat]] =
coll(_.byId[PerfStat](PerfStat.makeId(userId, perf)))

def insert(perfStat: PerfStat): Funit =
coll(_.insert.one(perfStat).void)
Expand Down
Loading

0 comments on commit 5640e11

Please sign in to comment.