Merge branch 'master' into coach
* master: fix analysis - initial node has no san - closes #2197 fix realtime user search more patron tracking remove site layout setup patron tracking don't load GA & GTM on the same page
This commit is contained in:
commit
e4b04e6eba
|
@ -45,16 +45,32 @@ object Plan extends LilaController {
|
|||
|
||||
private def renderIndex(email: Option[String], patron: Option[lila.plan.Patron])(implicit ctx: Context): Fu[Result] =
|
||||
Env.plan.api.recentChargeUserIds(50) zip
|
||||
Env.plan.api.topPatronUserIds(120) map {
|
||||
case (recentIds, bestIds) =>
|
||||
Env.plan.api.topPatronUserIds(120) zip
|
||||
ctx.me.??(me => makeTrackingUserData(me) map some) map {
|
||||
case ((recentIds, bestIds), trackingData) =>
|
||||
Ok(html.plan.index(
|
||||
stripePublicKey = Env.plan.stripePublicKey,
|
||||
email = email,
|
||||
patron = patron,
|
||||
recentIds = recentIds,
|
||||
bestIds = bestIds))
|
||||
bestIds = bestIds,
|
||||
trackingData = trackingData))
|
||||
}
|
||||
|
||||
private def makeTrackingUserData(me: UserModel) = for {
|
||||
tournaments <- Env.tournament.leaderboardApi.chart(me)
|
||||
nbFollowers <- Env.relation.api.countFollowers(me.id)
|
||||
nbFollowing <- Env.relation.api.countFollowing(me.id)
|
||||
nbForumPosts <- Env.forum.postApi.nbByUser(me.id)
|
||||
} yield lila.plan.PlanTrackingUserData(
|
||||
user = me,
|
||||
nbTournaments = tournaments.allPerfResults.nb,
|
||||
medianTournamentRank = tournaments.allPerfResults.rankPercentMedian,
|
||||
isStreamer = Env.tv.isStreamer(me.id),
|
||||
nbFollowers = nbFollowers,
|
||||
nbFollowing = nbFollowing,
|
||||
nbForumPosts = nbForumPosts)
|
||||
|
||||
private def indexPatron(me: UserModel, patron: lila.plan.Patron, customer: StripeCustomer)(implicit ctx: Context) =
|
||||
Env.plan.api.customerInfo(me, customer) flatMap {
|
||||
case Some(info: MonthlyCustomerInfo) => Ok(html.plan.indexStripe(me, patron, info)).fuccess
|
||||
|
|
|
@ -91,4 +91,6 @@ trait DateHelper { self: I18nHelper =>
|
|||
def atomDate(date: DateTime): String = atomDateFormatter print date
|
||||
def atomDate(field: String)(doc: io.prismic.Document): Option[String] =
|
||||
doc getDate field map (_.value.toDateTimeAtStartOfDay) map atomDate
|
||||
|
||||
def nowSeconds = (System.currentTimeMillis() / 1000).toInt
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ object Environment
|
|||
with scalalib.OrnicarOption
|
||||
with lila.BooleanSteroids
|
||||
with lila.OptionSteroids
|
||||
with lila.JodaTimeSteroids
|
||||
with StringHelper
|
||||
with JsonHelper
|
||||
with AssetHelper
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@(ex: Throwable)(implicit ctx: Context)
|
||||
|
||||
@site.layout(
|
||||
@base.layout(
|
||||
title = "Internal server error") {
|
||||
|
||||
<div class="content_box small_box">
|
||||
|
|
|
@ -68,7 +68,7 @@ withGtm: Boolean = false)(body: Html)(implicit ctx: Context)
|
|||
data-asset-url="@assetBaseUrl"
|
||||
data-asset-version="@assetVersion"
|
||||
data-accept-languages="@acceptLanguages.mkString(",")">
|
||||
@if(withGtm) { @gtm() }
|
||||
@if(withGtm) {@gtm()}
|
||||
<form id="blind_mode" action="@routes.Main.toggleBlindMode" method="POST">
|
||||
<input type="hidden" name="enable" value="@ctx.blindMode.fold(0,1)" />
|
||||
<input type="hidden" name="redirect" value="@ctx.req.path" />
|
||||
|
@ -209,6 +209,6 @@ withGtm: Boolean = false)(body: Html)(implicit ctx: Context)
|
|||
lichess_translations = @jsI18n()
|
||||
}
|
||||
}
|
||||
@base.ga()
|
||||
@if(!withGtm) {@base.ga()}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@()(implicit ctx: Context)
|
||||
|
||||
@site.layout(
|
||||
@base.layout(
|
||||
title = "Page not found",
|
||||
moreCss = cssTag("notFound.css")) {
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
@site.layout(
|
||||
@base.layout(
|
||||
title = title,
|
||||
side = side.some,
|
||||
moreCss = cssTag("features.css"),
|
||||
|
|
23
app/views/plan/gtm.scala.html
Normal file
23
app/views/plan/gtm.scala.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
@(data: lila.plan.PlanTrackingUserData)(implicit ctx: Context)
|
||||
|
||||
dataLayer.push("uid", "@data.user.id");
|
||||
dataLayer.push("Registration date", @data.user.createdAt.getSeconds);
|
||||
dataLayer.push("Total games played", @data.user.count.game);
|
||||
@data.user.perfs.latest.map { date =>
|
||||
dataLayer.push("Time since last game played", @(nowSeconds - date.getSeconds));
|
||||
}
|
||||
dataLayer.push("Average rating", @data.user.perfs.standard.intRating);
|
||||
dataLayer.push("Title", "@data.user.title");
|
||||
@data.medianTournamentRank.map { rank =>
|
||||
dataLayer.push("Average tournament rank", @rank);
|
||||
}
|
||||
dataLayer.push("Tournaments played", @data.nbTournaments);
|
||||
dataLayer.push("Tournament points", @data.user.toints);
|
||||
dataLayer.push("Streamer", @data.isStreamer);
|
||||
dataLayer.push("Login status", "logged in");
|
||||
dataLayer.push("Donation referrer page", "@lila.common.HTTPRequest.referer(ctx.req)");
|
||||
dataLayer.push("Followers", @data.nbFollowers);
|
||||
dataLayer.push("Following", @data.nbFollowing);
|
||||
dataLayer.push("Forum messages", @data.nbForumPosts);
|
||||
dataLayer.push("Seconds spent playing", @data.user.playTime.map(_.totalPeriod.getSeconds).getOrElse(0));
|
||||
dataLayer.push("Seconds featured on TV", @data.user.playTime.flatMap(_.tvPeriod.map(_.getSeconds)).getOrElse(0));
|
|
@ -1,4 +1,4 @@
|
|||
@(email: Option[String], stripePublicKey: String, patron: Option[lila.plan.Patron], recentIds: List[String], bestIds: List[String])(implicit ctx: Context)
|
||||
@(email: Option[String], stripePublicKey: String, patron: Option[lila.plan.Patron], recentIds: List[String], bestIds: List[String], trackingData: Option[lila.plan.PlanTrackingUserData])(implicit ctx: Context)
|
||||
|
||||
@title = @{"Become a Patron of lichess.org"}
|
||||
|
||||
|
@ -7,6 +7,9 @@
|
|||
@jsTag("checkout.js")
|
||||
@embedJs {
|
||||
lichess.checkout("@stripePublicKey");
|
||||
@trackingData.map { td =>
|
||||
@gtm(td)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,7 +24,7 @@ lichess.checkout("@stripePublicKey");
|
|||
</div>
|
||||
}
|
||||
|
||||
@site.layout(
|
||||
@base.layout(
|
||||
title = title,
|
||||
side = side.some,
|
||||
moreCss = cssTag("plan.css"),
|
||||
|
@ -29,7 +32,8 @@ moreJs = moreJs,
|
|||
openGraph = lila.app.ui.OpenGraph(
|
||||
title = title,
|
||||
url = s"$netBaseUrl${routes.Plan.index.url}",
|
||||
description = "Free chess for everyone, forever!").some) {
|
||||
description = "Free chess for everyone, forever!").some,
|
||||
withGtm = trackingData.isDefined) {
|
||||
<div class="content_box no_padding plan motivation">
|
||||
@patron.ifTrue(ctx.me.??(_.isPatron)).map { p =>
|
||||
<div class="banner one_time_active">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@(me: User, patron: lila.plan.Patron)(implicit ctx: Context)
|
||||
|
||||
@site.layout(
|
||||
@base.layout(
|
||||
title = "Thank you for your support!",
|
||||
moreCss = cssTag("plan.css")) {
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@(me: User, patron: lila.plan.Patron, info: lila.plan.MonthlyCustomerInfo)(implicit ctx: Context)
|
||||
|
||||
@site.layout(
|
||||
@base.layout(
|
||||
title = "Thank you for your support!",
|
||||
moreCss = cssTag("plan.css"),
|
||||
moreJs = jsTag("plan.js")) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@(patron: Option[lila.plan.Patron])(implicit ctx: Context)
|
||||
|
||||
@site.layout(
|
||||
@base.layout(
|
||||
moreCss = cssTag("page.css"),
|
||||
title = "Thank you for your support!") {
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
@title = @{ trans.reportAUser.str() }
|
||||
|
||||
@site.layout(title = title, moreCss = cssTag("report.css")) {
|
||||
@base.layout(title = title, moreCss = cssTag("report.css")) {
|
||||
<div class="content_box" id="report">
|
||||
<h1>@title</h1>
|
||||
<form class="create" action="@routes.Report.create()@reqUser.map(u => "?username=" + u.username)" method="post">
|
||||
|
|
|
@ -18,7 +18,7 @@ $button.find('span').text('Blocked!');
|
|||
}
|
||||
}
|
||||
|
||||
@site.layout(title = title, moreCss = cssTag("report.css"), moreJs = moreJs) {
|
||||
@base.layout(title = title, moreCss = cssTag("report.css"), moreJs = moreJs) {
|
||||
|
||||
<div class="content_box small_box">
|
||||
<h1 class="lichess_title">@title</h1>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
@import lila.gameSearch.{ Query, Sorting }
|
||||
|
||||
<form rel="nofollow" class="search" action="@routes.User.showFilter(u.username, "search")" method="get">
|
||||
<form rel="nofollow" class="search realtime" action="@routes.User.showFilter(u.username, "search")" method="get">
|
||||
<table>
|
||||
<tr class="header">
|
||||
<th colspan=2>@trans.advancedSearch()</th>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@()(implicit ctx: Context)
|
||||
|
||||
@site.layout(title = "Developers") {
|
||||
@base.layout(title = "Developers") {
|
||||
|
||||
<div class="content_box small_box">
|
||||
<h1 class="lichess_title">Embed lichess in your site</h1>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
@jsTag("lag.js")
|
||||
}
|
||||
|
||||
@site.layout(
|
||||
@base.layout(
|
||||
title = "Is lichess lagging?",
|
||||
moreCss = cssTag("lag.css"),
|
||||
moreJs = moreJs) {
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
@(title: String, moreCss: Html = Html(""), moreJs: Html = Html(""), side: Option[Html] = None, openGraph: Option[lila.app.ui.OpenGraph] = None)(body: Html)(implicit ctx: Context)
|
||||
|
||||
@base.layout(
|
||||
title = title,
|
||||
side = side,
|
||||
moreCss = moreCss,
|
||||
moreJs = moreJs,
|
||||
openGraph = openGraph)(body)
|
|
@ -1,6 +1,6 @@
|
|||
@(title: String, back: Boolean = true, icon: Option[Html] = None)(message: Html)(implicit ctx: Context)
|
||||
|
||||
@site.layout(title = title) {
|
||||
@base.layout(title = title) {
|
||||
|
||||
<div class="content_box small_box">
|
||||
<div class="head">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@(doc: io.prismic.Document, resolver: io.prismic.DocumentLinkResolver)(implicit ctx: Context)
|
||||
|
||||
@site.layout(
|
||||
@base.layout(
|
||||
moreCss = cssTag("page.css"),
|
||||
title = s"${~doc.getText("doc.title")}") {
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
@site.layout(
|
||||
@base.layout(
|
||||
moreCss = cssTag("swag.css"),
|
||||
side = side.some,
|
||||
title = "Lichess Swag",
|
||||
|
|
|
@ -11,6 +11,8 @@ case class Charge(
|
|||
cents: Cents,
|
||||
date: DateTime) {
|
||||
|
||||
def id = _id
|
||||
|
||||
def isPayPal = payPal.nonEmpty
|
||||
def isStripe = stripe.nonEmpty
|
||||
|
||||
|
|
|
@ -38,13 +38,16 @@ final class Env(
|
|||
goal = MonthlyGoalCents,
|
||||
chargeColl = chargeColl)
|
||||
|
||||
lazy val tracking = new PlanTracking
|
||||
|
||||
lazy val api = new PlanApi(
|
||||
stripeClient,
|
||||
patronColl = patronColl,
|
||||
chargeColl = chargeColl,
|
||||
notifier = notifier,
|
||||
tracking = tracking,
|
||||
lightUserApi = lightUserApi,
|
||||
bus,
|
||||
bus = bus,
|
||||
payPalIpnKey = PayPalIpnKey(config getString "paypal.ipn_key"),
|
||||
monthlyGoalApi = monthlyGoalApi)
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ final class PlanApi(
|
|||
stripeClient: StripeClient,
|
||||
patronColl: Coll,
|
||||
chargeColl: Coll,
|
||||
tracking: PlanTracking,
|
||||
notifier: PlanNotifier,
|
||||
lightUserApi: lila.user.LightUserApi,
|
||||
bus: lila.common.Bus,
|
||||
|
@ -56,21 +57,25 @@ final class PlanApi(
|
|||
}
|
||||
}
|
||||
|
||||
def onStripeCharge(charge: StripeCharge): Funit =
|
||||
customerIdPatron(charge.customer) flatMap { patronOption =>
|
||||
addCharge(Charge.make(
|
||||
def onStripeCharge(stripeCharge: StripeCharge): Funit =
|
||||
customerIdPatron(stripeCharge.customer) flatMap { patronOption =>
|
||||
val charge = Charge.make(
|
||||
userId = patronOption.map(_.userId),
|
||||
stripe = Charge.Stripe(charge.id, charge.customer).some,
|
||||
cents = charge.amount)) >> {
|
||||
stripe = Charge.Stripe(stripeCharge.id, stripeCharge.customer).some,
|
||||
cents = stripeCharge.amount)
|
||||
addCharge(charge) >> {
|
||||
patronOption match {
|
||||
case None =>
|
||||
logger.info(s"Charged anon customer $charge")
|
||||
funit
|
||||
case Some(patron) =>
|
||||
logger.info(s"Charged $charge $patron")
|
||||
stripeClient getCustomer stripeCharge.customer foreach {
|
||||
_ foreach { customer => tracking.charge(charge, renew = customer.renew) }
|
||||
}
|
||||
UserRepo byId patron.userId flatten s"Missing user for $patron" flatMap { user =>
|
||||
val p2 = patron.copy(
|
||||
stripe = Patron.Stripe(charge.customer).some
|
||||
stripe = Patron.Stripe(stripeCharge.customer).some
|
||||
).levelUpIfPossible
|
||||
patronColl.update($id(patron.id), p2) >>
|
||||
setDbUserPlan(user,
|
||||
|
@ -95,7 +100,7 @@ final class PlanApi(
|
|||
funit
|
||||
}
|
||||
else (cents.value >= 100) ?? {
|
||||
addCharge(Charge.make(
|
||||
val charge = Charge.make(
|
||||
userId = userId,
|
||||
payPal = Charge.PayPal(
|
||||
name = name,
|
||||
|
@ -103,7 +108,9 @@ final class PlanApi(
|
|||
txnId = txnId,
|
||||
subId = subId.map(_.value),
|
||||
ip = ip.some).some,
|
||||
cents = cents)) >>
|
||||
cents = cents)
|
||||
tracking.charge(charge, renew = subId.isDefined)
|
||||
addCharge(charge) >>
|
||||
(userId ?? UserRepo.named) flatMap { userOption =>
|
||||
userOption ?? { user =>
|
||||
val payPal = Patron.PayPal(email, subId, DateTime.now)
|
||||
|
@ -114,8 +121,11 @@ final class PlanApi(
|
|||
lastLevelUp = DateTime.now
|
||||
).expireInOneMonth) >>
|
||||
setDbUserPlan(user, lila.user.Plan.start) >>
|
||||
notifier.onStart(user)
|
||||
notifier.onStart(user) >>-
|
||||
tracking.newDonation(user, cents, renew = subId.isDefined)
|
||||
case Some(patron) =>
|
||||
if (subId.isDefined) tracking.upgrade(user, cents)
|
||||
else tracking.reDonation(user, cents)
|
||||
val p2 = patron.copy(
|
||||
payPal = payPal.some
|
||||
).levelUpIfPossible.expireInOneMonth
|
||||
|
@ -273,9 +283,12 @@ final class PlanApi(
|
|||
case None => createCustomer(user, data, plan) map { customer =>
|
||||
customer.firstSubscription err s"Can't create ${user.username} subscription for customer $customer"
|
||||
}
|
||||
case Some(customer) => setCustomerPlan(customer, plan, data.source) flatMap { sub =>
|
||||
saveStripePatron(user, customer.id, data.freq) inject sub
|
||||
}
|
||||
case Some(customer) =>
|
||||
if (data.freq.renew && !customer.renew) tracking.upgrade(user, plan.amount)
|
||||
if (!data.freq.renew) tracking.reDonation(user, plan.amount)
|
||||
setCustomerPlan(customer, plan, data.source) flatMap { sub =>
|
||||
saveStripePatron(user, customer.id, data.freq) inject sub
|
||||
}
|
||||
} flatMap { subscription =>
|
||||
logger.info(s"Subed user ${user.username} $subscription freq=${data.freq}")
|
||||
if (data.freq.renew) fuccess(subscription)
|
||||
|
@ -290,6 +303,7 @@ final class PlanApi(
|
|||
saveStripePatron(user, customer.id, data.freq) >>
|
||||
setDbUserPlan(user, lila.user.Plan.start) >>
|
||||
notifier.onStart(user) >>-
|
||||
tracking.newDonation(user, plan.amount, renew = data.freq.renew) >>-
|
||||
logger.info(s"Create ${user.username} customer $customer") inject customer
|
||||
}
|
||||
|
||||
|
@ -324,10 +338,7 @@ final class PlanApi(
|
|||
}
|
||||
|
||||
private def customerIdPatron(id: CustomerId): Fu[Option[Patron]] =
|
||||
patronColl.uno[Patron](selectStripeCustomerId(id))
|
||||
|
||||
private def selectStripeCustomerId(id: CustomerId): Bdoc =
|
||||
$doc("stripe.customerId" -> id)
|
||||
patronColl.uno[Patron]($doc("stripe.customerId" -> id))
|
||||
|
||||
def userPatron(user: User): Fu[Option[Patron]] = patronColl.uno[Patron]($id(user.id))
|
||||
}
|
||||
|
|
74
modules/plan/src/main/PlanTracking.scala
Normal file
74
modules/plan/src/main/PlanTracking.scala
Normal file
|
@ -0,0 +1,74 @@
|
|||
package lila.plan
|
||||
|
||||
import lila.user.User
|
||||
|
||||
import play.api.libs.ws.WS
|
||||
import play.api.Play.current
|
||||
|
||||
final class PlanTracking {
|
||||
|
||||
private val url = "https://www.google-analytics.com/collect"
|
||||
private val tid = "UA-7935029-3"
|
||||
private type PostArgs = Map[String, Seq[String]]
|
||||
|
||||
// user makes a one-time or recurring donation
|
||||
def newDonation(user: User, amount: Cents, renew: Boolean): Unit = {
|
||||
val args = makeArgs(Map(
|
||||
"ec" -> "Conversion",
|
||||
"ea" -> "Donate",
|
||||
"el" -> "Donation",
|
||||
"ev" -> amount.value,
|
||||
"uid" -> user.id))
|
||||
|
||||
send(args)
|
||||
send(args + ("el" -> Seq(
|
||||
if (renew) "Recurring donation" else "One-time donation"
|
||||
)))
|
||||
}
|
||||
|
||||
// user makes a second one-time donation
|
||||
def reDonation(user: User, amount: Cents): Unit = send(makeArgs(Map(
|
||||
"ec" -> "Conversion",
|
||||
"ea" -> "Donate",
|
||||
"el" -> "Redonation",
|
||||
"ev" -> amount.value,
|
||||
"uid" -> user.id)))
|
||||
|
||||
// user makes a recurring donation after a one-time donoation
|
||||
def upgrade(user: User, amount: Cents): Unit = send(makeArgs(Map(
|
||||
"ec" -> "Conversion",
|
||||
"ea" -> "Donate",
|
||||
"el" -> "Upgrade",
|
||||
"ev" -> amount.value,
|
||||
"uid" -> user.id)))
|
||||
|
||||
def charge(charge: Charge, renew: Boolean): Unit = send(makeArgs(Map(
|
||||
"ti" -> charge.id,
|
||||
"t" -> "transaction",
|
||||
"in" -> (if (renew) "Recurring payment" else "One-time payment"),
|
||||
"ip" -> charge.cents.usd.value,
|
||||
"tr" -> charge.cents.usd.value,
|
||||
"cu" -> "USD",
|
||||
"iq" -> 1,
|
||||
"ic" -> (if (renew) "RecurringDonationPaymentSKU" else "OneTimeDonationPaymentSKU"),
|
||||
"iv" -> "Donation",
|
||||
"ta" -> (if (charge.isPayPal) "PayPal" else "Stripe"),
|
||||
"uid" -> charge.userId)))
|
||||
|
||||
private def send(args: PostArgs): Unit =
|
||||
WS.url(url).post(args).effectFold(
|
||||
err => logger.warn(s"tracking $url $args", err), {
|
||||
case res if res.status == 200 =>
|
||||
case res => logger.warn(s"tracking $url $args ${res.status} ${res.body}")
|
||||
})
|
||||
|
||||
private def makeArgs(args: Map[String, Any]): PostArgs =
|
||||
baseArgs ++ args.mapValues(v => Seq(v.toString))
|
||||
|
||||
private def baseArgs: PostArgs = Map(
|
||||
"v" -> Seq("1"),
|
||||
"tid" -> Seq(tid),
|
||||
"cid" -> Seq(java.util.UUID.randomUUID.toString),
|
||||
"t" -> Seq("event"),
|
||||
"ds" -> Seq("backend"))
|
||||
}
|
12
modules/plan/src/main/PlanTrackingUserData.scala
Normal file
12
modules/plan/src/main/PlanTrackingUserData.scala
Normal file
|
@ -0,0 +1,12 @@
|
|||
package lila.plan
|
||||
|
||||
import lila.user.User
|
||||
|
||||
case class PlanTrackingUserData(
|
||||
user: User,
|
||||
isStreamer: Boolean,
|
||||
nbTournaments: Int,
|
||||
medianTournamentRank: Option[Int],
|
||||
nbFollowers: Int,
|
||||
nbFollowing: Int,
|
||||
nbForumPosts: Int)
|
|
@ -69,6 +69,8 @@ case class StripeCustomer(
|
|||
def firstSubscription = subscriptions.data.headOption
|
||||
|
||||
def plan = firstSubscription.map(_.plan)
|
||||
|
||||
def renew = firstSubscription ?? (_.renew)
|
||||
}
|
||||
|
||||
case class StripeCharge(id: ChargeId, amount: Cents, customer: CustomerId)
|
||||
|
|
|
@ -87,7 +87,7 @@ object LeaderboardApi {
|
|||
|
||||
case class ChartData(perfResults: List[(PerfType, ChartData.PerfResult)]) {
|
||||
import ChartData._
|
||||
def allPerfResults = perfResults.map(_._2) match {
|
||||
lazy val allPerfResults: PerfResult = perfResults.map(_._2) match {
|
||||
case head :: tail => tail.foldLeft(head) {
|
||||
case (acc, res) => PerfResult(
|
||||
nb = acc.nb + res.nb,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package lila.user
|
||||
|
||||
import reactivemongo.bson.BSONDocument
|
||||
import org.joda.time.DateTime
|
||||
|
||||
import chess.Speed
|
||||
import lila.db.BSON
|
||||
|
@ -143,6 +143,13 @@ case class Perfs(
|
|||
}
|
||||
}
|
||||
)
|
||||
|
||||
def latest: Option[DateTime] =
|
||||
perfsMap.values.toList.flatMap(_.latest).foldLeft(none[DateTime]) {
|
||||
case (None, date) => date.some
|
||||
case (Some(acc), date) if date isAfter acc => date.some
|
||||
case (acc, _) => acc
|
||||
}
|
||||
}
|
||||
|
||||
case object Perfs {
|
||||
|
@ -199,7 +206,7 @@ case object Perfs {
|
|||
|
||||
private def notNew(p: Perf): Option[Perf] = p.nb > 0 option p
|
||||
|
||||
def writes(w: BSON.Writer, o: Perfs) = BSONDocument(
|
||||
def writes(w: BSON.Writer, o: Perfs) = reactivemongo.bson.BSONDocument(
|
||||
"standard" -> notNew(o.standard),
|
||||
"chess960" -> notNew(o.chess960),
|
||||
"kingOfTheHill" -> notNew(o.kingOfTheHill),
|
||||
|
|
|
@ -84,4 +84,12 @@ $(function() {
|
|||
$form.submit(function() {
|
||||
$(this).addClass('searching');
|
||||
});
|
||||
|
||||
if ($form.hasClass('realtime')) {
|
||||
var submit = function() {
|
||||
$form.submit();
|
||||
};
|
||||
$form.find("select, input[type=checkbox]").change(submit);
|
||||
$usernames.bind("keyup", $.fp.debounce(submit, 2000));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -393,7 +393,7 @@ module.exports = function(opts) {
|
|||
if (this.vm.node.dests !== '') return false;
|
||||
if (this.vm.node.check) {
|
||||
var san = this.vm.node.san;
|
||||
var checkmate = san[san.length - 1] === '#';
|
||||
var checkmate = san && san[san.length - 1] === '#';
|
||||
return checkmate;
|
||||
}
|
||||
if (this.vm.node.crazy) {
|
||||
|
|
Loading…
Reference in a new issue