list of user studies
parent
f7d05677e7
commit
048b4befe8
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
|
@ -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)
|
|
@ -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)) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue