moar team stuff

This commit is contained in:
Thibault Duplessis 2012-12-11 19:43:35 +01:00
parent 2e5d7adbee
commit 51c4a3c0da
20 changed files with 271 additions and 72 deletions

View file

@ -14,16 +14,21 @@ import play.api.templates.Html
object Team extends LilaController {
private def repo = env.team.repo
private def teamRepo = env.team.teamRepo
private def forms = env.team.forms
private def api = env.team.api
private def paginator = env.team.paginator
def home(page: Int) = Open { implicit ctx
Ok(html.team.home(api popular page))
Ok(html.team.home(paginator popularTeams page))
}
def show(id: String) = Open { implicit ctx
IOptionOk(repo byId id) { html.team.show(_) }
def show(id: String, page: Int) = Open { implicit ctx
IOptionIOk(teamRepo byId id) { team
api isMine team map { isMine
html.team.show(team, paginator.teamMembers(team, page), isMine)
}
}
}
def form = Auth { implicit ctx
@ -46,11 +51,22 @@ object Team extends LilaController {
}
def mine = Auth { implicit ctx
me IOk(repo byUser me map { html.team.mine(_) })
me IOk(teamRepo byUser me map { html.team.mine(_) })
}
def join(id: String) = TODO
def leave(id: String) = TODO
def join(id: String) = Auth { implicit ctx
implicit me IOResult(api join id map {
case Some(team) Redirect(routes.Team.show(team.id))
case _ notFound
})
}
def quit(id: String) = Auth { implicit ctx
implicit me IOResult(api quit id map {
case Some(team) Redirect(routes.Team.show(team.id))
case _ notFound
})
}
private def OnePerWeek[A <: Result](me: UserModel)(a: A)(implicit ctx: Context): Result = {
!Granter.superAdmin(me) &&

View file

@ -53,6 +53,7 @@ final class CoreEnv private (application: Application, val settings: Settings) {
lazy val team = new lila.team.TeamEnv(
settings = settings,
captcha = site.captcha,
userRepo = user.userRepo,
mongodb = mongodb.apply _)
lazy val lobby = new lila.lobby.LobbyEnv(

View file

@ -124,6 +124,7 @@ final class Settings(config: Config, val IsDev: Boolean) {
val WikiGitUrl = getString("wiki.git_url")
val TeamCollectionTeam = getString("team.collection.team")
val TeamCollectionMember = getString("team.collection.member")
val TeamPaginatorMaxPerPage = getInt("team.paginator.max_per_page")
val BookmarkCollectionBookmark = getString("bookmark.collection.bookmark")

View file

@ -171,7 +171,7 @@ final class I18nKeys(translator: Translator) {
val myTeams = new Key("myTeams")
val noTeamFound = new Key("noTeamFound")
val joinTeam = new Key("joinTeam")
val leaveTeam = new Key("leaveTeam")
val quitTeam = new Key("quitTeam")
def keys = List(playWithAFriend, inviteAFriendToPlayWithYou, playWithTheMachine, challengeTheArtificialIntelligence, toInviteSomeoneToPlayGiveThisUrl, gameOver, waitingForOpponent, waiting, yourTurn, aiNameLevelAiLevel, level, toggleTheChat, toggleSound, chat, resign, checkmate, stalemate, white, black, createAGame, noGameAvailableRightNowCreateOne, whiteIsVictorious, blackIsVictorious, playWithTheSameOpponentAgain, newOpponent, playWithAnotherOpponent, yourOpponentWantsToPlayANewGameWithYou, joinTheGame, whitePlays, blackPlays, theOtherPlayerHasLeftTheGameYouCanForceResignationOrWaitForHim, makeYourOpponentResign, forceResignation, talkInChat, theFirstPersonToComeOnThisUrlWillPlayWithYou, whiteCreatesTheGame, blackCreatesTheGame, whiteJoinsTheGame, blackJoinsTheGame, whiteResigned, blackResigned, whiteLeftTheGame, blackLeftTheGame, shareThisUrlToLetSpectatorsSeeTheGame, youAreViewingThisGameAsASpectator, replayAndAnalyse, viewGameStats, flipBoard, threefoldRepetition, claimADraw, offerDraw, draw, nbConnectedPlayers, talkAboutChessAndDiscussLichessFeaturesInTheForum, seeTheGamesBeingPlayedInRealTime, gamesBeingPlayedRightNow, viewAllNbGames, viewNbCheckmates, nbBookmarks, nbPopularGames, nbAnalysedGames, bookmarkedByNbPlayers, viewInFullSize, logOut, signIn, signUp, people, games, forum, chessPlayers, minutesPerSide, variant, timeControl, start, username, password, haveAnAccount, allYouNeedIsAUsernameAndAPassword, learnMoreAboutLichess, rank, gamesPlayed, declineInvitation, cancel, timeOut, drawOfferSent, drawOfferDeclined, drawOfferAccepted, drawOfferCanceled, yourOpponentOffersADraw, accept, decline, playingRightNow, abortGame, gameAborted, standard, unlimited, mode, casual, rated, thisGameIsRated, rematch, rematchOfferSent, rematchOfferAccepted, rematchOfferCanceled, rematchOfferDeclined, cancelRematchOffer, viewRematch, play, inbox, chatRoom, spectatorRoom, composeMessage, sentMessages, incrementInSeconds, freeOnlineChess, spectators, nbWins, nbLosses, nbDraws, exportGames, color, eloRange, giveNbSeconds, searchAPlayer, whoIsOnline, allPlayers, namedPlayers, premoveEnabledClickAnywhereToCancel, thisPlayerUsesChessComputerAssistance, opening, takeback, proposeATakeback, takebackPropositionSent, takebackPropositionDeclined, takebackPropositionAccepted, takebackPropositionCanceled, yourOpponentProposesATakeback, bookmarkThisGame, toggleBackground, advancedSearch, tournament, freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents, teams, nbMembers, allTeams, newTeam, myTeams, noTeamFound, joinTeam, leaveTeam)
def keys = List(playWithAFriend, inviteAFriendToPlayWithYou, playWithTheMachine, challengeTheArtificialIntelligence, toInviteSomeoneToPlayGiveThisUrl, gameOver, waitingForOpponent, waiting, yourTurn, aiNameLevelAiLevel, level, toggleTheChat, toggleSound, chat, resign, checkmate, stalemate, white, black, createAGame, noGameAvailableRightNowCreateOne, whiteIsVictorious, blackIsVictorious, playWithTheSameOpponentAgain, newOpponent, playWithAnotherOpponent, yourOpponentWantsToPlayANewGameWithYou, joinTheGame, whitePlays, blackPlays, theOtherPlayerHasLeftTheGameYouCanForceResignationOrWaitForHim, makeYourOpponentResign, forceResignation, talkInChat, theFirstPersonToComeOnThisUrlWillPlayWithYou, whiteCreatesTheGame, blackCreatesTheGame, whiteJoinsTheGame, blackJoinsTheGame, whiteResigned, blackResigned, whiteLeftTheGame, blackLeftTheGame, shareThisUrlToLetSpectatorsSeeTheGame, youAreViewingThisGameAsASpectator, replayAndAnalyse, viewGameStats, flipBoard, threefoldRepetition, claimADraw, offerDraw, draw, nbConnectedPlayers, talkAboutChessAndDiscussLichessFeaturesInTheForum, seeTheGamesBeingPlayedInRealTime, gamesBeingPlayedRightNow, viewAllNbGames, viewNbCheckmates, nbBookmarks, nbPopularGames, nbAnalysedGames, bookmarkedByNbPlayers, viewInFullSize, logOut, signIn, signUp, people, games, forum, chessPlayers, minutesPerSide, variant, timeControl, start, username, password, haveAnAccount, allYouNeedIsAUsernameAndAPassword, learnMoreAboutLichess, rank, gamesPlayed, declineInvitation, cancel, timeOut, drawOfferSent, drawOfferDeclined, drawOfferAccepted, drawOfferCanceled, yourOpponentOffersADraw, accept, decline, playingRightNow, abortGame, gameAborted, standard, unlimited, mode, casual, rated, thisGameIsRated, rematch, rematchOfferSent, rematchOfferAccepted, rematchOfferCanceled, rematchOfferDeclined, cancelRematchOffer, viewRematch, play, inbox, chatRoom, spectatorRoom, composeMessage, sentMessages, incrementInSeconds, freeOnlineChess, spectators, nbWins, nbLosses, nbDraws, exportGames, color, eloRange, giveNbSeconds, searchAPlayer, whoIsOnline, allPlayers, namedPlayers, premoveEnabledClickAnywhereToCancel, thisPlayerUsesChessComputerAssistance, opening, takeback, proposeATakeback, takebackPropositionSent, takebackPropositionDeclined, takebackPropositionAccepted, takebackPropositionCanceled, yourOpponentProposesATakeback, bookmarkThisGame, toggleBackground, advancedSearch, tournament, freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents, teams, nbMembers, allTeams, newTeam, myTeams, noTeamFound, joinTeam, quitTeam)
}

View file

@ -1,22 +1,35 @@
package lila
package team
import org.joda.time.{ DateTime, Duration }
import com.novus.salat.annotations.Key
import org.joda.time.DateTime
import org.scala_tools.time.Imports._
import scalaz.effects._
import user.User
import user.{ User, UserRepo }
case class Member(
id: String,
createdAt: DateTime) {
@Key("_id") id: String,
team: String,
user: String,
date: DateTime) {
def is(userId: String): Boolean = id == userId
def is(userId: String): Boolean = user == userId
def is(user: User): Boolean = is(user.id)
}
object Member {
def apply(user: User): Member = apply(user.id)
def makeId(team: String, user: String) = user + "@" + team
def apply(user: String): Member = new Member(id = user, createdAt = DateTime.now)
def apply(team: String, user: String): Member = new Member(
id = makeId(team, user),
user = user,
team = team,
date = DateTime.now)
}
case class MemberWithUser(member: Member, user: User) {
def team = member.team
def date = member.date
}

55
app/team/MemberRepo.scala Normal file
View file

@ -0,0 +1,55 @@
package lila
package team
import com.novus.salat._
import com.novus.salat.dao._
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import scalaz.effects._
import org.joda.time.DateTime
// db.member.ensureIndex({t:1})
// db.member.ensureIndex({u:1})
// db.member.ensureIndex({d: -1})
final class MemberRepo(collection: MongoCollection)
extends SalatDAO[Member, String](collection) {
// def byTeamId(teamId: String): IO[List[Member]] = io {
// find(teamIdQuery(teamId)).toList
// }
def userIdsByTeamId(teamId: String): IO[List[String]] = io {
(collection find teamIdQuery(teamId) sort sortQuery(1) map { obj
obj.getAs[String]("user")
}).flatten.toList
}
def teamIdsByUserId(userId: String): IO[Set[String]] = io {
(collection find userIdQuery(userId) map { obj
obj.getAs[String]("team")
}).flatten.toSet
}
def removeByteamId(teamId: String): IO[Unit] = io {
remove(teamIdQuery(teamId))
}
def exists(teamId: String, userId: String): IO[Boolean] = io {
collection.find(idQuery(teamId, userId)).limit(1).size != 0
}
def idQuery(teamId: String, userId: String) = DBObject("_id" -> id(teamId, userId))
def id(teamId: String, userId: String) = Member.makeId(teamId, userId)
def teamIdQuery(teamId: String) = DBObject("team" -> teamId)
def userIdQuery(userId: String) = DBObject("user" -> userId)
def sortQuery(order: Int = -1) = DBObject("date" -> order)
def add(teamId: String, userId: String): IO[Unit] = io {
insert(Member(team = teamId, user = userId))
}
def remove(teamId: String, userId: String): IO[Unit] = io {
remove(idQuery(teamId, userId))
}
}

View file

@ -0,0 +1,48 @@
package lila
package team
import user.{ User, UserRepo }
import mongodb.CachedAdapter
import com.github.ornicar.paginator._
import com.mongodb.casbah.Imports._
import org.joda.time.DateTime
final class PaginatorBuilder(
memberRepo: MemberRepo,
teamRepo: TeamRepo,
userRepo: UserRepo,
maxPerPage: Int) {
def popularTeams(page: Int): Paginator[Team] = paginator(
SalatAdapter(
dao = teamRepo,
query = teamRepo.enabledQuery,
sort = teamRepo.sortPopular), page)
def teamMembers(team: Team, page: Int): Paginator[MemberWithUser] =
paginator(new TeamAdapter(team), page)
private def paginator[A](adapter: Adapter[A], page: Int): Paginator[A] =
Paginator(
adapter,
currentPage = page,
maxPerPage = maxPerPage
) | paginator(adapter, 1)
final class TeamAdapter(team: Team) extends Adapter[MemberWithUser] {
val nbResults = team.nbMembers
def slice(offset: Int, length: Int): Seq[MemberWithUser] = {
val members = (memberRepo find query sort sort skip offset limit length).toList
val users = (userRepo byOrderedIds members.map(_.user)).unsafePerformIO
members zip users map {
case (member, user) MemberWithUser(member, user)
}
}
private def query = memberRepo teamIdQuery team.id
private def sort = memberRepo sortQuery -1
}
}

View file

@ -6,13 +6,13 @@ import user.User
import org.joda.time.DateTime
import com.novus.salat.annotations.Key
import java.text.Normalizer
import scalaz.effects._
case class Team(
@Key("_id") id: String, // also the url slug
name: String,
location: Option[String],
description: String,
members: List[Member],
nbMembers: Int,
enabled: Boolean,
createdAt: DateTime,
@ -20,11 +20,6 @@ case class Team(
def slug = id
def contains(userId: String): Boolean = members exists (_ is userId)
def contains(user: User): Boolean = contains(user.id)
def canJoin(user: User) = true
def disabled = !enabled
}
@ -39,11 +34,10 @@ object Team {
name = name,
location = location,
description = description,
members = Member(createdBy) :: Nil,
nbMembers = 1,
enabled = true,
createdAt = DateTime.now,
createdBy = createdBy.id)
def nameToId(name: String) = templating.StringHelper slugify name
def nameToId(name: String) = templating.StringHelper slugify name
}

View file

@ -2,36 +2,62 @@ package lila
package team
import scalaz.effects._
import com.github.ornicar.paginator._
import org.scala_tools.time.Imports._
import com.github.ornicar.paginator.Paginator
import user.User
import user.{ User, UserRepo }
import http.Context
final class TeamApi(
repo: TeamRepo,
maxPerPage: Int) {
teamRepo: TeamRepo,
memberRepo: MemberRepo,
userRepo: UserRepo,
paginator: PaginatorBuilder) {
val creationPeriod = 1 week
def popular(page: Int): Paginator[Team] = Paginator(
SalatAdapter(
dao = repo,
query = repo.enabledQuery,
sort = repo.sortPopular),
currentPage = page,
maxPerPage = maxPerPage
) | popular(1)
def create(setup: TeamSetup, me: User): IO[Team] = setup.trim |> { s
Team(
name = s.name,
location = s.location,
description = s.description,
createdBy = me) |> { team
repo saveIO team inject team
teamRepo saveIO team inject team
}
}
def hasCreatedRecently(me: User): IO[Boolean] =
repo.userHasCreatedSince(me.id, creationPeriod)
teamRepo.userHasCreatedSince(me.id, creationPeriod)
def isMine(team: Team)(implicit ctx: Context): IO[Boolean] =
~ctx.me.map(me belongsTo(team, me))
def join(teamId: String)(implicit ctx: Context): IO[Option[Team]] = for {
teamOption teamRepo byId teamId
result ~(teamOption |@| ctx.me).tupled.map({
case (team, user) for {
exists belongsTo(team, user)
_ (for {
_ memberRepo.add(team.id, user.id)
_ teamRepo.incMembers(team.id, +1)
} yield ()) doUnless exists
} yield team.some
})
} yield result
def quit(teamId: String)(implicit ctx: Context): IO[Option[Team]] = for {
teamOption teamRepo byId teamId
result ~(teamOption |@| ctx.me).tupled.map({
case (team, user) for {
exists belongsTo(team, user)
_ (for {
_ memberRepo.remove(team.id, user.id)
_ teamRepo.incMembers(team.id, -1)
} yield ()) doIf exists
} yield team.some
})
} yield result
def belongsTo(team: Team, user: User): IO[Boolean] =
memberRepo.exists(teamId = team.id, userId = user.id)
}

View file

@ -3,21 +3,33 @@ package team
import core.Settings
import site.Captcha
import user.UserRepo
import com.mongodb.casbah.MongoCollection
final class TeamEnv(
settings: Settings,
captcha: Captcha,
userRepo: UserRepo,
mongodb: String MongoCollection) {
import settings._
lazy val repo = new TeamRepo(mongodb(TeamCollectionTeam))
lazy val teamRepo = new TeamRepo(mongodb(TeamCollectionTeam))
lazy val api = new TeamApi(
repo = repo,
lazy val memberRepo = new MemberRepo(mongodb(TeamCollectionMember))
lazy val paginator = new PaginatorBuilder(
memberRepo = memberRepo,
teamRepo = teamRepo,
userRepo = userRepo,
maxPerPage = TeamPaginatorMaxPerPage)
lazy val forms = new DataForm(repo, captcha)
lazy val api = new TeamApi(
teamRepo = teamRepo,
memberRepo = memberRepo,
userRepo = userRepo,
paginator = paginator)
lazy val forms = new DataForm(teamRepo, captcha)
}

View file

@ -42,6 +42,10 @@ final class TeamRepo(collection: MongoCollection)
).limit(1).size > 0
}
def incMembers(teamId: String, by: Int): IO[Unit] = io {
update(selectId(teamId), $inc("nbMembers" -> by))
}
def userQuery(user: User) = DBObject("members.id" -> user.id)
def selectId(id: String) = DBObject("_id" -> id)

View file

@ -9,7 +9,7 @@ currentTab = "mine".some) {
@if(teams.size > 0) {
<tbody>
@teams.map { team =>
<tr class="paginated_element">
<tr>
<td class="subject">
<a class="team-name" href="@routes.Team.show(team.id)">@team.name</a>
@shorten(team.description, 200)

View file

@ -1,20 +1,12 @@
@(t: lila.team.Team)(implicit ctx: Context)
@(t: lila.team.Team, members: Paginator[lila.team.MemberWithUser], isMine: Boolean)(implicit ctx: Context)
@team.layout(
title = t.name) {
<div class="content_box no_padding team_show">
<div id="team" class="content_box no_padding team_show">
<div class="content_box_top">
@if(~ctx.me.map(t.contains)) {
<a href="@routes.Team.leave(t.id)" class="send_message">@trans.leaveTeam()</a>
} else {
if (~ctx.me.map(t.canJoin)) {
<a href="@routes.Team.join(t.id)" class="send_message">@trans.joinTeam()</a>
}
}
<span class="righty">@trans.nbMembers(t.nbMembers)</span>
<div class="icon"></div>
<h1 class="lichess_title">@t.name</h1>
<span class="rank">
@trans.nbMembers(t.nbMembers)
</span>
@if(t.disabled) {
<span class="staff">CLOSED</span>
}
@ -22,5 +14,28 @@ title = t.name) {
<div class="content_box_content clearfix">
@autoLink(t.description)
</div>
@if(t.enabled && !isMine) {
<form class="inline" method="post" action="@routes.Team.join(t.id)">
<input class="submit button" type="submit" value="@trans.joinTeam()" />
</form>
}
@if(isMine) {
<form class="inline" method="post" action="@routes.Team.quit(t.id)">
<input class="submit button" type="submit" value="@trans.quitTeam()" />
</form>
}
<div class="team-members">
<table class="slist">
<tbody>
@members.currentPageResults.map { member =>
<tr class="paginated_element">
<td class="subject">
@userLink(member.user)
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}

View file

@ -67,7 +67,7 @@ evenMoreCss = evenMoreCss) {
<form id="exportform" action="@routes.User.export(u.username)" method="POST">
<a onclick="document.getElementById('exportform').submit();">@trans.exportGames()</a>
</form>
<div class="status @isUsernameOnline(u.username).fold("connected", "")"></div>
<div class="icon status @isUsernameOnline(u.username).fold("connected", "")"></div>
<h1 class="lichess_title">@u.usernameWithElo</h1>
@info.rank.map { r =>
<span class="rank">

View file

@ -147,4 +147,4 @@ newTeam=New team
myTeams=My teams
noTeamFound=No team found
joinTeam=Join team
leaveTeam=Leave team
quitTeam=Quit team

View file

@ -55,9 +55,9 @@ GET /team controllers.Team.home(page: Int ?=
GET /team/new controllers.Team.form
POST /team/new controllers.Team.create
GET /team/me controllers.Team.mine
GET /team/:id controllers.Team.show(id: String)
GET /team/:id controllers.Team.show(id: String, page: Int ?= 1)
POST /team/:id/join controllers.Team.join(id: String)
POST /team/:id/leave controllers.Team.leave(id: String)
POST /team/:id/quit controllers.Team.quit(id: String)
# Analyse
GET /analyse/$gameId<[\w\-]{8}> controllers.Analyse.replay(gameId: String, color: String = "white")

View file

@ -17,7 +17,7 @@ trait Resolvers {
trait Dependencies {
val scalaz = "org.scalaz" %% "scalaz-core" % "6.0.4"
val salat = "com.novus" %% "salat-core" % "1.9.1"
val scalalib = "com.github.ornicar" %% "scalalib" % "2.5"
val scalalib = "com.github.ornicar" %% "scalalib" % "2.7"
val config = "com.typesafe" % "config" % "0.4.1"
val guava = "com.google.guava" % "guava" % "13.0"
val apache = "org.apache.commons" % "commons-lang3" % "3.1"

View file

@ -174,6 +174,7 @@ div.content_box.no_padding h1 {
.content_box_title {
padding: 20px 25px 0 25px;
}
div.content_box_top {
padding-left: 25px;
height: 40px;
@ -193,6 +194,18 @@ div.content_box_top a {
div.content_box_top .link:hover {
background: #fff;
}
div.content_box_top h1.lichess_title {
display: inline;
padding: 0;
}
div.content_box_top .icon {
display: block;
width: 16px;
height: 16px;
float: left;
background: transparent url(/assets/images/s16.png) top left no-repeat;
margin: 12px 8px 0 -10px;
}
.content_box_content {
padding: 20px 25px 25px 25px;
}

View file

@ -46,3 +46,13 @@ form.new-team p.error {
font-size: 0.9em;
white-space: nowrap;
}
#team div.content_box_top .icon {
background-position: 0 -336px;
}
#team.team_show .lichess_title {
font-size: 1.8em;
}
#team.team_show .content_box_top .righty {
float: right;
margin-right: 20px;
}

View file

@ -1,16 +1,7 @@
div.user_show .lichess_title {
display: inline;
}
div.user_show div.content_box_top .status {
display: block;
width: 16px;
height: 16px;
float: left;
background: transparent url(/assets/images/s16.png) top left no-repeat;
div.user_show div.content_box_top .icon {
background-position: 0 -160px;
margin: 12px 8px 0 -10px;
}
div.user_show div.content_box_top .status.connected {
div.user_show div.content_box_top .icon.connected {
background-position: 0 -208px;
}
div.user_show .staff {