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:
Thibault Duplessis 2016-08-21 19:48:18 +02:00
commit e4b04e6eba
31 changed files with 208 additions and 51 deletions

View file

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

View file

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

View file

@ -17,6 +17,7 @@ object Environment
with scalalib.OrnicarOption
with lila.BooleanSteroids
with lila.OptionSteroids
with lila.JodaTimeSteroids
with StringHelper
with JsonHelper
with AssetHelper

View file

@ -1,6 +1,6 @@
@(ex: Throwable)(implicit ctx: Context)
@site.layout(
@base.layout(
title = "Internal server error") {
<div class="content_box small_box">

View file

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

View file

@ -1,6 +1,6 @@
@()(implicit ctx: Context)
@site.layout(
@base.layout(
title = "Page not found",
moreCss = cssTag("notFound.css")) {

View file

@ -38,7 +38,7 @@
</div>
}
@site.layout(
@base.layout(
title = title,
side = side.some,
moreCss = cssTag("features.css"),

View 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));

View file

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

View file

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

View file

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

View file

@ -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!") {

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@
@jsTag("lag.js")
}
@site.layout(
@base.layout(
title = "Is lichess lagging?",
moreCss = cssTag("lag.css"),
moreJs = moreJs) {

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@
</div>
}
@site.layout(
@base.layout(
moreCss = cssTag("swag.css"),
side = side.some,
title = "Lichess Swag",

View file

@ -11,6 +11,8 @@ case class Charge(
cents: Cents,
date: DateTime) {
def id = _id
def isPayPal = payPal.nonEmpty
def isStripe = stripe.nonEmpty

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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