From 048b4befe8fadf88bed7c024fae26156021fcad6 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 11 May 2016 18:21:03 +0200 Subject: [PATCH] list of user studies --- app/Env.scala | 1 + app/controllers/Study.scala | 16 +++++ app/mashup/UserInfo.scala | 6 +- app/views/study/byMember.scala.html | 11 ++++ app/views/study/byOwner.scala.html | 11 ++++ app/views/study/list.scala.html | 28 +++++++++ app/views/study/listLayout.scala.html | 16 +++++ app/views/user/show.scala.html | 3 + conf/routes | 5 +- modules/study/src/main/BSONHandlers.scala | 2 +- modules/study/src/main/ChapterRepo.scala | 20 +++++- modules/study/src/main/Env.scala | 4 ++ modules/study/src/main/Study.scala | 2 + modules/study/src/main/StudyPager.scala | 46 ++++++++++++++ modules/study/src/main/StudyRepo.scala | 26 ++------ modules/user/src/main/User.scala | 2 + public/stylesheets/studyList.css | 74 +++++++++++++++++++++++ 17 files changed, 248 insertions(+), 25 deletions(-) create mode 100644 app/views/study/byMember.scala.html create mode 100644 app/views/study/byOwner.scala.html create mode 100644 app/views/study/list.scala.html create mode 100644 app/views/study/listLayout.scala.html create mode 100644 modules/study/src/main/StudyPager.scala create mode 100644 public/stylesheets/studyList.css diff --git a/app/Env.scala b/app/Env.scala index e1b344062f..77686c1bfe 100644 --- a/app/Env.scala +++ b/app/Env.scala @@ -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, diff --git a/app/controllers/Study.scala b/app/controllers/Study.scala index 68eeaa2bbc..11a92017ae 100644 --- a/app/controllers/Study.scala +++ b/app/controllers/Study.scala @@ -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) diff --git a/app/mashup/UserInfo.scala b/app/mashup/UserInfo.scala index 426be269aa..89a815689c 100644 --- a/app/mashup/UserInfo.scala +++ b/app/mashup/UserInfo.scala @@ -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, diff --git a/app/views/study/byMember.scala.html b/app/views/study/byMember.scala.html new file mode 100644 index 0000000000..67107d54f8 --- /dev/null +++ b/app/views/study/byMember.scala.html @@ -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) { +
+

Studies @userLink(member) contributes to

+ @list(pag) +
+} diff --git a/app/views/study/byOwner.scala.html b/app/views/study/byOwner.scala.html new file mode 100644 index 0000000000..b6c22ed0a1 --- /dev/null +++ b/app/views/study/byOwner.scala.html @@ -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) { +
+

Studies created by @userLink(owner)

+ @list(pag) +
+} diff --git a/app/views/study/list.scala.html b/app/views/study/list.scala.html new file mode 100644 index 0000000000..c15ea1517d --- /dev/null +++ b/app/views/study/list.scala.html @@ -0,0 +1,28 @@ +@(pag: Paginator[lila.study.Study.WithChapters])(implicit ctx: Context) +
+ @pag.currentPageResults.map { s => +
+ +

+ + @s.study.name + + @if(s.study.isPublic){Public}else{Private}, + @momentFromNow(s.study.createdAt) + +

+
+
    + @s.chapters.take(4).map { name => +
  1. @name
  2. + } +
+
    + @s.study.members.members.values.take(4).map { m => +
  1. @usernameOrId(m.id)
  2. + } +
+
+
+ } +
diff --git a/app/views/study/listLayout.scala.html b/app/views/study/listLayout.scala.html new file mode 100644 index 0000000000..6d791f810a --- /dev/null +++ b/app/views/study/listLayout.scala.html @@ -0,0 +1,16 @@ +@(title: String, active: String, user: lila.user.User)(body: Html)(implicit ctx: Context) + +@moreCss = { +@cssTag("studyList.css") +} + +@menu = { +Own studies +Contributed studies +} + +@base.layout( +title = title, +menu = menu.some, +moreCss = moreCss, +withLangAnnotations = false)(body) diff --git a/app/views/user/show.scala.html b/app/views/user/show.scala.html index b21a77412b..4d33b0d060 100644 --- a/app/views/user/show.scala.html +++ b/app/views/user/show.scala.html @@ -56,6 +56,9 @@ description = describeUser(u)).some) { @splitNumber(nb + " Blockers") } @splitNumber(u.toints + " " + trans.tournamentPoints()) + @if(info.nbStudies > 0) { + @splitNumber(info.nbStudies + " studies") + } @splitNumber(trans.nbForumPosts(info.nbPosts)) @NotForKids { @if(ctx.isAuth && !ctx.is(u)) { diff --git a/conf/routes b/conf/routes index 3947becde4..e591f45770 100644 --- a/conf/routes +++ b/conf/routes @@ -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) diff --git a/modules/study/src/main/BSONHandlers.scala b/modules/study/src/main/BSONHandlers.scala index 2b73ab13dd..c8f8a8bb84 100644 --- a/modules/study/src/main/BSONHandlers.scala +++ b/modules/study/src/main/BSONHandlers.scala @@ -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) } diff --git a/modules/study/src/main/ChapterRepo.scala b/modules/study/src/main/ChapterRepo.scala index 74b2a87a72..2ef45d79e8 100644 --- a/modules/study/src/main/ChapterRepo.scala +++ b/modules/study/src/main/ChapterRepo.scala @@ -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) - } +} diff --git a/modules/study/src/main/Env.scala b/modules/study/src/main/Env.scala index 3f55310a08..ae0c4c7e7e 100644 --- a/modules/study/src/main/Env.scala +++ b/modules/study/src/main/Env.scala @@ -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, diff --git a/modules/study/src/main/Study.scala b/modules/study/src/main/Study.scala index 60acf0ef00..b85782928e 100644 --- a/modules/study/src/main/Study.scala +++ b/modules/study/src/main/Study.scala @@ -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 diff --git a/modules/study/src/main/StudyPager.scala b/modules/study/src/main/StudyPager.scala new file mode 100644 index 0000000000..a8f62420ce --- /dev/null +++ b/modules/study/src/main/StudyPager.scala @@ -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)) + } + } +} diff --git a/modules/study/src/main/StudyRepo.scala b/modules/study/src/main/StudyRepo.scala index abcc6b3feb..0fb78b85ae 100644 --- a/modules/study/src/main/StudyRepo.scala +++ b/modules/study/src/main/StudyRepo.scala @@ -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) diff --git a/modules/user/src/main/User.scala b/modules/user/src/main/User.scala index b2d282bf41..aadbd439b0 100644 --- a/modules/user/src/main/User.scala +++ b/modules/user/src/main/User.scala @@ -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 diff --git a/public/stylesheets/studyList.css b/public/stylesheets/studyList.css new file mode 100644 index 0000000000..90ab6e1d76 --- /dev/null +++ b/public/stylesheets/studyList.css @@ -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; +}