Complete user module

pull/83/head
Thibault Duplessis 2013-03-13 22:41:43 +01:00
parent 5213003e4b
commit ef9975e445
23 changed files with 262 additions and 120 deletions

View File

@ -2,7 +2,7 @@ package lila
import ornicar.scalalib
import scalaz.Zero
import scalaz.{ Zero, Functor }
import scala.concurrent.Future
trait PackageObject
@ -66,6 +66,7 @@ trait WithFuture extends scalaz.Zeros {
type Funit = Fu[Unit]
def fuccess[A](a: A) = Future successful a
def fufail[A <: Throwable](a: A) = Future failed a
def funit = fuccess(())
implicit def FuZero[A: Zero]: Zero[Fu[A]] = new Zero[Fu[A]] { val zero = fuccess([A]) }
@ -73,9 +74,10 @@ trait WithFuture extends scalaz.Zeros {
trait WithDb { self: PackageObject
type LilaDB = reactivemongo.api.DB
type ReactiveColl = reactivemongo.api.collections.default.BSONCollection
// implicit def reactiveSortJsObject(sort: (String, SortOrder)): (String, JsValueWrapper) = sort match {
// case (field, SortOrder.Ascending) field -> 1
// case (field, _) field -> -1
// }
}
trait WithPlay { self: PackageObject
@ -91,6 +93,10 @@ trait WithPlay { self: PackageObject ⇒
type JsEnumerator = Enumerator[JsValue]
type SocketFuture = Future[(Iteratee[JsValue, _], JsEnumerator)]
implicit def FutureFunctor = new Functor[Fu] {
def fmap[A, B](r: Fu[A], f: A B) = r map f
}
implicit def lilaRicherFuture[A](fua: Fu[A]) = new {
def >>[B](fub: Fu[B]): Fu[B] = fua flatMap (_ fub)
@ -100,7 +106,7 @@ trait WithPlay { self: PackageObject ⇒
def inject[B](b: B): Fu[B] = fua map (_ b)
}
implicit def lilaRicherFutureZero[A : Zero](fua: Fu[A]) = new {
implicit def lilaRicherFutureZero[A: Zero](fua: Fu[A]) = new {
def doIf(cond: Boolean): Fu[A] = cond.fold(fua, fuccess([A]))

View File

@ -0,0 +1,102 @@
package lila.common
package paginator
import scalaz.Success
import play.api.libs.concurrent.Execution.Implicits._
trait AdapterLike[A] {
/**
* Returns the total number of results.
*/
def nbResults: Fu[Int]
/**
* Returns a slice of the results.
*
* @param offset The number of elements to skip, starting from zero
* @param length The maximum number of elements to return
*/
def slice(offset: Int, length: Int): Fu[Seq[A]]
/**
* FUNCTOR INTERFACE
*/
def map[B](f: A B): AdapterLike[B] = new AdapterLike[B] {
def nbResults = AdapterLike.this.nbResults
def slice(offset: Int, length: Int) = AdapterLike.this.slice(offset, length) map2 f
}
}
final class Paginator[A] private[paginator] (
val currentPage: Int,
val maxPerPage: Int,
/**
* Returns the results for the current page.
* The result is cached.
*/
val currentPageResults: Seq[A],
/**
* Returns the number of results.
* The result is cached.
*/
val nbResults: Int) {
/**
* Returns the previous page.
*/
def previousPage: Option[Int] =
if (currentPage == 1) None else Some(currentPage - 1)
/**
* Returns the next page.
*/
def nextPage: Option[Int] =
if (currentPage == nbPages) None else Some(currentPage + 1)
/**
* FUNCTOR INTERFACE
*/
def map[B](f: A B): Paginator[B] = new Paginator(
currentPage = currentPage,
maxPerPage = maxPerPage,
currentPageResults = currentPageResults map f,
nbResults = nbResults)
/**
* Returns the number of pages.
*/
def nbPages: Int = scala.math.ceil(nbResults.toFloat / maxPerPage).toInt
/**
* Returns whether we have to paginate or not.
* This is true if the number of results is higher than the max per page.
*/
def hasToPaginate: Boolean = nbResults > maxPerPage
/**
* Returns whether there is previous page or not.
*/
def hasPreviousPage: Boolean = None != previousPage
/**
* Returns whether there is next page or not.
*/
def hasNextPage: Boolean = None != nextPage
}
object Paginator {
def apply[A](
adapter: AdapterLike[A],
currentPage: Int = 1,
maxPerPage: Int = 10): Valid[Fu[Paginator[A]]] =
if (currentPage <= 0) !!("Max per page must be greater than zero")
else if (maxPerPage <= 0) !!("Current page must be greater than zero")
else Success(for {
results adapter.slice((currentPage - 1) * maxPerPage, maxPerPage)
nbResults adapter.nbResults
} yield new Paginator(currentPage, maxPerPage, results, nbResults))
}

View File

@ -1,20 +0,0 @@
package lila.db
import com.github.ornicar.paginator._
import com.novus.salat.dao.DAO
import com.novus.salat._
import com.mongodb.casbah.Imports.DBObject
final class CachedAdapter[A <: CaseClass, B <: Any](
dao: DAO[A, B],
query: DBObject,
sort: DBObject,
val nbResults: Int) extends Adapter[A] {
private val salatAdapter = SalatAdapter(
dao = dao,
query = query,
sort = sort)
def slice(offset: Int, length: Int) = salatAdapter.slice(offset, length)
}

View File

@ -1,11 +0,0 @@
package lila
import reactivemongo.api.collections.GenericQueryBuilder
import reactivemongo.bson._
package object db extends PackageObject with WithPlay with WithDb {
type WithStringId = { def id: String }
type QueryBuilder = GenericQueryBuilder[BSONDocument, BSONDocumentReader, BSONDocumentWriter]
}

View File

@ -0,0 +1,41 @@
package lila.db
import play.api.libs.json._, Json.JsValueWrapper
import reactivemongo.api.collections.GenericQueryBuilder
import reactivemongo.bson._
import reactivemongo.api._
import reactivemongo.api.SortOrder
import play.modules.reactivemongo.Implicits._
object Implicits extends Implicits
trait Implicits {
type LilaDB = reactivemongo.api.DB
type ReactiveColl = reactivemongo.api.collections.default.BSONCollection
type WithStringId = { def id: String }
type QueryBuilder = GenericQueryBuilder[BSONDocument, BSONDocumentReader, BSONDocumentWriter]
// hack, this should be in reactivemongo
implicit def richerQueryBuilder(b: QueryBuilder) = new {
def sort(sorters: (String, SortOrder)*): QueryBuilder =
if (sorters.size == 0) b
else b sort {
BSONDocument(
(for (sorter sorters) yield sorter._1 -> BSONInteger(
sorter._2 match {
case SortOrder.Ascending 1
case SortOrder.Descending -1
})).toStream)
}
// def sortJs(sorter: JsObject): QueryBuilder = b sort JsObjectWriter.write(sorter)
def limit(nb: Int): QueryBuilder = b.options(b.options batchSize nb)
def skip(nb: Int): QueryBuilder = b.options(b.options skip nb)
}
}

View File

@ -0,0 +1,25 @@
package lila.db
package paginator
import lila.common.paginator.AdapterLike
import play.api.libs.json._
import reactivemongo.api.SortOrder
final class Adapter[A <: WithStringId](
repo: Repo[A],
query: JsObject,
sort: Seq[(String, SortOrder)]) extends AdapterLike[A] {
def nbResults: Fu[Int] = repo count query
def slice(offset: Int, length: Int): Fu[Seq[A]] = repo find {
richerQueryBuilder(repo query query).sort(sort: _*) skip offset limit length
}
}
final class CachedAdapter[A <: WithStringId](
adapter: Adapter[A],
val nbResults: Fu[Int]) extends AdapterLike[A] {
def slice(offset: Int, length: Int) = adapter.slice(offset, length)
}

View File

@ -11,7 +11,7 @@ import play.api.libs.concurrent.Execution.Implicits._
import scala.concurrent.Future
abstract class Coll[Doc <: WithStringId](coll: ReactiveColl, json: JsonTube[Doc]) extends DbApi {
abstract class Repo[Doc <: WithStringId](coll: ReactiveColl, json: JsonTube[Doc]) extends DbApi {
object query {
@ -123,23 +123,6 @@ abstract class Coll[Doc <: WithStringId](coll: ReactiveColl, json: JsonTube[Doc]
type ID = String
// hack, this should be in reactivemongo
protected implicit def richerQueryBuilder(b: QueryBuilder) = new {
def sort(sorters: (String, SortOrder)*): QueryBuilder =
if (sorters.size == 0) b
else b sort {
BSONDocument(
(for (sorter sorters) yield sorter._1 -> BSONInteger(
sorter._2 match {
case SortOrder.Ascending 1
case SortOrder.Descending -1
})).toStream)
}
def limit(nb: Int): QueryBuilder = b.options(b.options batchSize nb)
}
//////////////////
// PRIVATE SHIT //
//////////////////
@ -160,5 +143,5 @@ abstract class Coll[Doc <: WithStringId](coll: ReactiveColl, json: JsonTube[Doc]
def read(bson: BSONDocument): Option[Doc] = json.fromMongo(JsObjectReader read bson).asOpt
}
private def fuck(msg: Any) = Future failed (new DbException(msg.toString))
private def fuck(msg: Any) = fufail(new DbException(msg.toString))
}

View File

@ -0,0 +1,6 @@
package lila
package object db
extends PackageObject
with WithPlay
with db.Implicits

View File

@ -27,8 +27,6 @@ trait Dependencies {
val slf4jNop = "org.slf4j" %% "slf4j-nop" % "1.6.4"
val guava = "com.google.guava" % "guava" % "14.0"
val findbugs = "com.google.code.findbugs" % "jsr305" % "2.0.1"
val paginator = "com.github.ornicar" % "paginator-core_2.9.1" % "1.6"
val paginatorSalat = "com.github.ornicar" % "paginator-salat-adapter_2.9.1" % "1.5"
val csv = "com.github.tototoshi" % "scala-csv_2.9.1" % "0.3"
val hasher = "hasher" %% "hasher" % "0.3.1"
val jgit = "org.eclipse.jgit" % "org.eclipse.jgit" % "1.3.0.201202151440-r"
@ -37,7 +35,7 @@ trait Dependencies {
val jodaConvert = "org.joda" % "joda-convert" % "1.2"
val scalastic = "scalastic" % "scalastic_2.9.2" % "0.20.1-THIB"
val reactivemongo = "org.reactivemongo" %% "reactivemongo" % "0.9-SNAPSHOT"
val playReactivemongo = "org.reactivemongo" %% "play2-reactivemongo" % "0.9-SNAPSHOT"
val playReactivemongo = "org.reactivemongo" %% "play2-reactivemongo" % "0.9-SNAPSHOT"
val playProvided = "play" %% "play" % "2.1-SNAPSHOT" % "provided"
val playTestProvided = "play" %% "play-test" % "2.1-SNAPSHOT" % "provided"
val sprayCaching = "io.spray" % "spray-caching" % "1.1-M7"
@ -58,11 +56,16 @@ object ApplicationBuild extends Build with Resolvers with Dependencies {
"-language:_")
)
private val srcMain = Seq(
scalaSource in Compile <<= (sourceDirectory in Compile)(identity),
scalaSource in Test <<= (sourceDirectory in Test)(identity)
)
lazy val lila = play.Project("lila", "4",
settings = buildSettings ++ Seq(
libraryDependencies := Seq(
scalaz, scalalib, hasher, config, salat, apache, scalaTime,
paginator, paginatorSalat, csv, jgit, actuarius, scalastic, findbugs,
csv, jgit, actuarius, scalastic, findbugs,
reactivemongo),
templatesImport ++= Seq(
"lila.app.game.{ DbGame, DbPlayer, Pov }",
@ -71,8 +74,8 @@ object ApplicationBuild extends Build with Resolvers with Dependencies {
"lila.app.templating.Environment._",
"lila.app.ui",
"lila.app.http.Context",
"com.github.ornicar.paginator.Paginator")
)) dependsOn (user) aggregate (scalachess, common, db, user)
"lila.common.paginator.Paginator")
)) dependsOn (user, wiki) aggregate (scalachess, common, db, user, wiki)
lazy val common = project("common").settings(
libraryDependencies := Seq(
@ -85,26 +88,26 @@ object ApplicationBuild extends Build with Resolvers with Dependencies {
lazy val db = project("db", Seq(common)).settings(
libraryDependencies := Seq(
scalaz, scalalib, playProvided, salat, reactivemongo,
paginator, paginatorSalat, playReactivemongo
scalaz, scalalib, playProvided, salat, reactivemongo, playReactivemongo
)
)
).settings(srcMain: _*)
lazy val user = project("user", Seq(common, memo, db, scalachess)).settings(
libraryDependencies := Seq(
scalaz, scalalib, hasher, jodaTime, jodaConvert, playProvided, salat,
paginator, paginatorSalat, sprayCaching, playTestProvided,
reactivemongo, playReactivemongo),
scalaSource in Compile <<= (sourceDirectory in Compile)(identity),
scalaSource in Test <<= (sourceDirectory in Test)(identity)
)
scalaz, scalalib, hasher, jodaTime, jodaConvert, playProvided,
sprayCaching, playTestProvided, reactivemongo, playReactivemongo)
).settings(srcMain: _*)
lazy val wiki = project("wiki", Seq(common, db)).settings(
libraryDependencies := Seq(
scalaz, scalalib, jodaTime, jodaConvert, playProvided,
playTestProvided, reactivemongo, playReactivemongo)
).settings(srcMain: _*)
lazy val scalachess = project("scalachess").settings(
libraryDependencies := Seq(scalaz, scalalib, hasher, jodaTime, jodaConvert)
)
private def project(name: String, deps: Seq[sbt.ClasspathDep[sbt.ProjectReference]] = Seq.empty) =
Project(name, file(name), dependencies = deps, settings = buildSettings ++ Seq(
// scalaSource in Compile <<= (sourceDirectory in Compile)(identity)
))
Project(name, file(name), dependencies = deps, settings = buildSettings)
}

View File

@ -1,26 +0,0 @@
package lila.user
import com.github.ornicar.paginator._
import com.mongodb.casbah.Imports.DBObject
import lila.db.CachedAdapter
final class PaginatorBuilder(userRepo: UserRepo, countUsers: () Int, maxPerPage: Int) {
def elo(page: Int): Paginator[User] =
paginator(recentAdapter, page)
private val recentAdapter =
adapter(DBObject("enabled" -> true))
private def adapter(query: DBObject) = new CachedAdapter(
dao = userRepo,
query = query,
sort = DBObject("elo" -> -1),
nbResults = countUsers())
private def paginator(adapter: Adapter[User], page: Int) = Paginator(
adapter,
currentPage = page,
maxPerPage = maxPerPage).fold(_ elo(0), identity)
}

View File

@ -18,7 +18,7 @@ final class Cached(userRepo: UserRepo, ttl: Duration) {
def usernameOrAnonymous(id: Option[String]): String = (id flatMap username) | Users.anonymous
def countEnabled: Int = cache.fromFuture("count-enabled")(userRepo.countEnabled).await
def countEnabled: Fu[Int] = cache.fromFuture("count-enabled")(userRepo.countEnabled)
// id => username
private val usernameCache = mutable.Map[String, Option[String]]()

View File

@ -1,11 +1,12 @@
package lila.user
import scala.math.round
import scalaz.effects._
import play.api.libs.json.Json
import org.joda.time.DateTime
import org.joda.time.format.{ DateTimeFormat, DateTimeFormatter }
import play.api.libs.concurrent.Execution.Implicits._
final class EloChart(rawElos: List[(Int, Int, Option[Int])]) {
private val points = 100
@ -62,10 +63,10 @@ object EloChart {
Json.arr("number", "Average")
)
def apply(historyRepo: HistoryRepo)(user: User): IO[Option[EloChart]] =
def apply(historyRepo: HistoryRepo)(user: User): Fu[Option[EloChart]] =
historyRepo userElos user.username map { elos
(elos.size > 1) option {
new EloChart((user.createdAt.getSeconds.toInt, User.STARTING_ELO, None) :: elos)
new EloChart((user.createdAt.getSeconds.toInt, Users.STARTING_ELO, None) :: elos.toList)
}
}
}

View File

@ -1,6 +1,7 @@
package lila.user
import lila.db.{ Coll, DbApi }
import lila.db.DbApi
import lila.db.Implicits._
import play.api.libs.json._
import play.api.libs.concurrent.Execution.Implicits._

View File

@ -0,0 +1,29 @@
package lila.user
import lila.common.paginator._
import lila.db.paginator._
import play.api.libs.json._
final class PaginatorBuilder(userRepo: UserRepo, countUsers: Fu[Int], maxPerPage: Int) {
def elo(page: Int): Fu[Paginator[User]] = paginator(recentAdapter, page)
private val recentAdapter: AdapterLike[User] = adapter(Json.obj("enabled" -> true))
private def adapter(query: JsObject): AdapterLike[User] = new CachedAdapter(
adapter = new Adapter(
repo = userRepo,
query = query,
sort = Seq(userRepo.sortEloDesc)
),
nbResults = countUsers
)
private def paginator(adapter: AdapterLike[User], page: Int): Fu[Paginator[User]] =
Paginator(
adapter,
currentPage = page,
maxPerPage = maxPerPage
).fold(_ elo(0), identity)
}

View File

@ -1,5 +1,6 @@
package lila.user
import lila.db.ReactiveColl
import com.mongodb.casbah.MongoCollection
import akka.actor.ActorSystem
@ -9,25 +10,25 @@ final class UserEnv(settings: Settings, db: String ⇒ ReactiveColl) {
import settings._
// lazy val historyRepo = new HistoryRepo(mongodb(CollectionHistory))
lazy val historyRepo = new HistoryRepo(db(CollectionHistory))
lazy val userRepo = new UserRepo(db(CollectionUser))
// lazy val paginator = new PaginatorBuilder(
// userRepo = userRepo,
// countUsers = () cached.countEnabled,
// maxPerPage = PaginatorMaxPerPage)
lazy val paginator = new PaginatorBuilder(
userRepo = userRepo,
countUsers = cached.countEnabled,
maxPerPage = PaginatorMaxPerPage)
// lazy val eloUpdater = new EloUpdater(
// userRepo = userRepo,
// historyRepo = historyRepo,
// floor = EloUpdaterFloor)
lazy val eloUpdater = new EloUpdater(
userRepo = userRepo,
historyRepo = historyRepo,
floor = EloUpdaterFloor)
lazy val usernameMemo = new UsernameMemo(ttl = OnlineTtl)
// lazy val cached = new Cached(
// userRepo = userRepo,
// ttl = CachedNbTtl)
lazy val cached = new Cached(
userRepo = userRepo,
ttl = CachedNbTtl)
// lazy val eloChart = EloChart(historyRepo) _
lazy val eloChart = EloChart(historyRepo) _
}

View File

@ -1,6 +1,7 @@
package lila.user
import lila.db.{ Coll, DbApi }
import lila.db.{ Repo, DbApi }
import lila.db.Implicits._
import play.api.libs.json.Json
import play.api.libs.concurrent.Execution.Implicits._
@ -16,7 +17,7 @@ import com.roundeights.hasher.Implicits._
import org.joda.time.DateTime
import ornicar.scalalib.Random
final class UserRepo(coll: ReactiveColl) extends Coll[User](coll, Users.json) {
final class UserRepo(coll: ReactiveColl) extends Repo[User](coll, Users.json) {
def normalize(id: ID) = id.toLowerCase
@ -32,7 +33,7 @@ final class UserRepo(coll: ReactiveColl) extends Coll[User](coll, Users.json) {
val enabledQuery = Json.obj("enabled" -> true)
val sortEloDesc = ("elo" -> sort.desc)
val sortEloDesc = "elo" -> sort.desc
def incNbGames(id: ID, rated: Boolean, ai: Boolean, result: Option[Int]) = {
val incs = List(