list of user studies

pull/1880/head
Thibault Duplessis 2016-05-11 18:21:03 +02:00
parent f7d05677e7
commit 048b4befe8
17 changed files with 248 additions and 25 deletions

View File

@ -39,6 +39,7 @@ final class Env(
gameCached = Env.game.cached,
crosstableApi = Env.game.crosstableApi,
postApi = Env.forum.postApi,
studyRepo = Env.study.studyRepo,
getRatingChart = Env.history.ratingChartApi.apply,
getRanks = Env.user.cached.ranking.getAll,
isDonor = Env.donation.isDonor,

View File

@ -13,6 +13,22 @@ object Study extends LilaController {
private def env = Env.study
def byOwner(username: String, page: Int) = Open { implicit ctx =>
OptionFuOk(lila.user.UserRepo named username) { owner =>
env.pager.byOwnerForUser(owner.id, ctx.me, page) map { pag =>
html.study.byOwner(pag, owner)
}
}
}
def byMember(username: String, page: Int) = Open { implicit ctx =>
OptionFuOk(lila.user.UserRepo named username) { member =>
env.pager.byMemberForUser(member.id, ctx.me, page) map { pag =>
html.study.byMember(pag, member)
}
}
}
def show(id: String) = Open { implicit ctx =>
val query = get("chapterId").fold(env.api byIdWithChapter id) { chapterId =>
env.api.byIdWithChapter(id, chapterId)

View File

@ -25,6 +25,7 @@ case class UserInfo(
nbFollowers: Int,
nbBlockers: Option[Int],
nbPosts: Int,
nbStudies: Int,
playTime: User.PlayTime,
donor: Boolean,
trophies: Trophies,
@ -61,6 +62,7 @@ object UserInfo {
gameCached: lila.game.Cached,
crosstableApi: lila.game.CrosstableApi,
postApi: PostApi,
studyRepo: lila.study.StudyRepo,
getRatingChart: User => Fu[Option[String]],
getRanks: String => Fu[Map[String, Int]],
isDonor: String => Fu[Boolean],
@ -77,11 +79,12 @@ object UserInfo {
relationApi.countFollowers(user.id) zip
(ctx.me ?? Granter(_.UserSpy) ?? { relationApi.countBlockers(user.id) map (_.some) }) zip
postApi.nbByUser(user.id) zip
studyRepo.countByOwner(user.id) zip
isDonor(user.id) zip
trophyApi.findByUser(user) zip
(user.count.rated >= 10).??(insightShare.grant(user, ctx.me)) zip
getPlayTime(user) flatMap {
case ((((((((((((nbUsers, ranks), nbPlaying), nbImported), crosstable), ratingChart), nbFollowers), nbBlockers), nbPosts), isDonor), trophies), insightVisible), playTime) =>
case (((((((((((((nbUsers, ranks), nbPlaying), nbImported), crosstable), ratingChart), nbFollowers), nbBlockers), nbPosts), nbStudies), isDonor), trophies), insightVisible), playTime) =>
(nbPlaying > 0) ?? isHostingSimul(user.id) map { hasSimul =>
new UserInfo(
user = user,
@ -96,6 +99,7 @@ object UserInfo {
nbFollowers = nbFollowers,
nbBlockers = nbBlockers,
nbPosts = nbPosts,
nbStudies = nbStudies,
playTime = playTime,
donor = isDonor,
trophies = trophies,

View File

@ -0,0 +1,11 @@
@(pag: Paginator[lila.study.Study.WithChapters], member: User)(implicit ctx: Context)
@listLayout(
title = s"Studies ${member.titleName} contributes to",
active = "member",
user = member) {
<div class="content_box no_padding">
<h1>Studies @userLink(member) contributes to</h1>
@list(pag)
</div>
}

View File

@ -0,0 +1,11 @@
@(pag: Paginator[lila.study.Study.WithChapters], owner: User)(implicit ctx: Context)
@listLayout(
title = s"Studies created by ${owner.titleName}",
active = "owner",
user = owner) {
<div class="content_box no_padding">
<h1>Studies created by @userLink(owner)</h1>
@list(pag)
</div>
}

View File

@ -0,0 +1,28 @@
@(pag: Paginator[lila.study.Study.WithChapters])(implicit ctx: Context)
<div class="studies">
@pag.currentPageResults.map { s =>
<div class="study">
<a class="overlay" href="@routes.Study.show(s.study.id)"></a>
<h2>
<i data-icon=""></i>
<strong>@s.study.name</strong>
<span>
@if(s.study.isPublic){Public}else{Private},
@momentFromNow(s.study.createdAt)
</span>
</h2>
<div class="body">
<ol class="chapters">
@s.chapters.take(4).map { name =>
<li class="text" data-icon="K">@name</li>
}
</ol>
<ol class="members">
@s.study.members.members.values.take(4).map { m =>
<li class="text" data-icon="@if(m.canContribute){}else{v}">@usernameOrId(m.id)</li>
}
</ol>
</div>
</div>
}
</div>

View File

@ -0,0 +1,16 @@
@(title: String, active: String, user: lila.user.User)(body: Html)(implicit ctx: Context)
@moreCss = {
@cssTag("studyList.css")
}
@menu = {
<a class="@active.active("owner")" href="@routes.Study.byOwner(user.username)">Own studies</a>
<a class="@active.active("member")" href="@routes.Study.byMember(user.username)">Contributed studies</a>
}
@base.layout(
title = title,
menu = menu.some,
moreCss = moreCss,
withLangAnnotations = false)(body)

View File

@ -56,6 +56,9 @@ description = describeUser(u)).some) {
<a class="intertab">@splitNumber(nb + " Blockers")</a>
}
<a href="@routes.UserTournament.path(u.username, "recent")" class="intertab tournament_stats" data-toints="@u.toints">@splitNumber(u.toints + " " + trans.tournamentPoints())</a>
@if(info.nbStudies > 0) {
<a href="@routes.Study.byOwner(u.username)" class="intertab">@splitNumber(info.nbStudies + " studies")</a>
}
<a class="intertab" href="@routes.ForumPost.search("user:" + u.username, 1)">@splitNumber(trans.nbForumPosts(info.nbPosts))</a>
@NotForKids {
@if(ctx.isAuth && !ctx.is(u)) {

View File

@ -47,8 +47,10 @@ GET /insights/:username controllers.Insight.index(username: Strin
GET /insights/:username/:metric/:dimension controllers.Insight.path(username: String, metric: String, dimension: String, filters: String = "")
GET /insights/:username/:metric/:dimension/*filters controllers.Insight.path(username: String, metric: String, dimension: String, filters: String)
# UserTournament
# User subpages
GET /@/:username/tournaments/:path controllers.UserTournament.path(username: String, path: String, page: Int ?= 1)
GET /@/:username/study controllers.Study.byOwner(username: String, page: Int ?= 1)
GET /@/:username/study/member controllers.Study.byMember(username: String, page: Int ?= 1)
# User
GET /@/:username/opponents controllers.User.opponents(username: String)
@ -118,6 +120,7 @@ GET /analysis/*something controllers.UserAnalysis.parse(something:
GET /analysis controllers.UserAnalysis.index
POST /analysis/pgn controllers.UserAnalysis.pgn
# Study
GET /study/$id<\w{8}> controllers.Study.show(id: String)
POST /study controllers.Study.create
GET /study/$id<\w{8}>/socket/v:apiVersion controllers.Study.websocket(id: String, apiVersion: Int)

View File

@ -181,7 +181,7 @@ private object BSONHandlers {
def write(x: StudyMembers) = $doc(x.members.mapValues(StudyMemberBSONWriter.write))
}
import Study.Visibility
implicit val visibilityHandler = new BSONHandler[BSONString, Visibility] {
private[study] implicit val VisibilityHandler: BSONHandler[BSONString, Visibility] = new BSONHandler[BSONString, Visibility] {
def read(bs: BSONString) = Visibility.byKey get bs.value err s"Invalid visibility ${bs.value}"
def write(x: Visibility) = BSONString(x.key)
}

View File

@ -33,6 +33,24 @@ final class ChapterRepo(coll: Coll) {
"order"
) map { order => ~order + 1 }
def namesByStudyIds(studyIds: Seq[Study.ID]): Fu[Map[Study.ID, Vector[String]]] =
coll.find(
$doc("studyId" -> $doc("$in" -> studyIds)),
$doc("studyId" -> true, "name" -> true)
).sort($sort asc "order").list[Bdoc]().map { docs =>
docs.foldLeft(Map.empty[Study.ID, Vector[String]]) {
case (hash, doc) => {
for {
studyId <- doc.getAs[String]("studyId")
name <- doc.getAs[String]("name")
} yield hash + (studyId -> (hash.get(studyId) match {
case None => Vector(name)
case Some(names) => names :+ name
}))
} | hash
}
}
def countByStudyId(studyId: Study.ID): Fu[Int] =
coll.countSel($studyId(studyId))
@ -43,4 +61,4 @@ final class ChapterRepo(coll: Coll) {
def delete(id: Chapter.ID): Funit = coll.remove($id(id)).void
private def $studyId(id: Study.ID) = $doc("studyId" -> id)
}
}

View File

@ -74,6 +74,10 @@ final class Env(
chat = hub.actor.chat,
socketHub = socketHub)
lazy val pager = new StudyPager(
studyRepo = studyRepo,
chapterRepo = chapterRepo)
lazy val pgnDump = new PgnDump(
chapterRepo = chapterRepo,
gamePgnDump = gamePgnDump,

View File

@ -47,6 +47,8 @@ object Study {
case class WithChapter(study: Study, chapter: Chapter)
case class WithChapters(study: Study, chapters: Seq[String])
type ID = String
val idSize = 8

View File

@ -0,0 +1,46 @@
package lila.study
import lila.common.paginator.Paginator
import lila.db.dsl._
import lila.db.paginator.Adapter
import lila.user.User
final class StudyPager(
studyRepo: StudyRepo,
chapterRepo: ChapterRepo) {
import BSONHandlers._
import studyRepo.{ selectPublic, selectMemberId, selectOwnerId }
def whereUidsContain(userId: User.ID, page: Int) = paginator($doc("uids" -> userId), page)
def byOwner(ownerId: User.ID, page: Int) = paginator($doc("ownerId" -> ownerId), page)
def byOwnerForUser(ownerId: User.ID, user: Option[User], page: Int) = paginator(
selectOwnerId(ownerId) ++ accessSelect(user), page)
def byMemberForUser(memberId: User.ID, user: Option[User], page: Int) = paginator(
selectMemberId(memberId) ++ $doc("ownerId" $ne memberId) ++ accessSelect(user), page)
def accessSelect(user: Option[User]) =
user.fold(selectPublic) { u =>
$or(selectPublic, selectMemberId(u.id))
}
private def paginator(selector: Bdoc, page: Int): Fu[Paginator[Study.WithChapters]] = Paginator(
adapter = new Adapter[Study](
collection = studyRepo.coll,
selector = selector,
projection = $empty,
sort = $sort desc "createdAt"
) mapFutureList withChapters,
currentPage = page,
maxPerPage = 10)
private def withChapters(studies: Seq[Study]): Fu[Seq[Study.WithChapters]] =
chapterRepo namesByStudyIds studies.map(_.id) map { chapters =>
studies.map { study =>
Study.WithChapters(study, ~(chapters get study.id))
}
}
}

View File

@ -3,12 +3,10 @@ package lila.study
import org.joda.time.DateTime
import scala.concurrent.duration._
import lila.common.paginator.Paginator
import lila.db.dsl._
import lila.db.paginator.Adapter
import lila.user.User
final class StudyRepo(coll: Coll) {
final class StudyRepo(private[study] val coll: Coll) {
import BSONHandlers._
@ -16,25 +14,11 @@ final class StudyRepo(coll: Coll) {
def exists(id: Study.ID) = coll.exists($id(id))
private val sortRecent = $sort desc "createdAt"
private[study] def selectOwnerId(ownerId: User.ID) = $doc("ownerId" -> ownerId)
private[study] def selectMemberId(memberId: User.ID) = $doc("uids" -> memberId)
private[study] val selectPublic = $doc("visibility" -> VisibilityHandler.write(Study.Visibility.Public))
def whereUidsContain(userId: User.ID, page: Int): Fu[Paginator[Study]] = Paginator(
adapter = new Adapter[Study](
collection = coll,
selector = $doc("uids" -> userId),
projection = $empty,
sort = sortRecent),
currentPage = page,
maxPerPage = 10)
def byOwner(userId: User.ID, page: Int): Fu[Paginator[Study]] = Paginator(
adapter = new Adapter[Study](
collection = coll,
selector = $doc("ownerId" -> userId),
projection = $empty,
sort = sortRecent),
currentPage = page,
maxPerPage = 10)
def countByOwner(ownerId: User.ID) = coll.countSel(selectOwnerId(ownerId))
def insert(s: Study): Funit = coll.insert {
StudyBSONHandler.write(s) ++ $doc("uids" -> s.members.ids)

View File

@ -38,6 +38,8 @@ case class User(
def light = LightUser(id = id, name = username, title = title)
def titleName = title.fold(username)(_ + " " + username)
def langs = ("en" :: lang.toList).distinct.sorted
def compare(other: User) = id compare other.id

View File

@ -0,0 +1,74 @@
.studies {
display: flex;
flex-flow: row wrap;
}
.studies .study {
flex: 0 0 50%;
max-width: 50%;
border-top: 1px solid #ccc;
box-sizing: border-box;
padding: 10px 10px 20px 10px;
position: relative;
}
.studies .study:nth-child(odd) {
border-right: 1px solid #ccc;
}
.studies .study:hover {
background: rgba(191, 231, 255, 0.5);
}
.studies .study a.overlay {
position: absolute;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.studies .body {
display: flex;
}
.studies .study ol {
flex: 0 0 50%;
max-width: 50%;
}
.studies .study ol li {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.studies .study ol li::before {
opacity: 0.5;
}
.study h2 {
display: block;
position: relative;
box-sizing: border-box;
height: 60px;
padding-left: 64px
}
.study h2 strong {
font-size: 1.5em;
font-weight: normal;
color: #3893E8;
display: block;
margin: 11px 0 1px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.study h2 span {
font-size: 1em;
display: block;
opacity: 0.8;
}
.studies h2 i {
position: absolute;
top: 0;
left: 15px;
}
.studies h2 i::before {
color: #3893E8;
font-size: 34px;
line-height: 43px;
opacity: 0.8;
}