teams wip

This commit is contained in:
Thibault Duplessis 2012-12-11 04:11:39 +01:00
parent 38d3325a5f
commit d663aa8a83
29 changed files with 270 additions and 122 deletions

View file

@ -1,8 +1,10 @@
package controllers
import lila._
import user.{ User UserModel }
import views._
import http.Context
import security.Granter
import scalaz.effects._
import play.api.mvc._
@ -25,22 +27,31 @@ object Team extends LilaController {
}
def form = Auth { implicit ctx
me
Ok(html.team.form(forms.create, forms.captchaCreate))
me OnePerWeek(me) {
Ok(html.team.form(forms.create, forms.captchaCreate))
}
}
def create = TODO
// AuthBody { implicit ctx
// implicit me
// NoEngine {
// IOResult {
// implicit val req = ctx.body
// forms.create.bindFromRequest.fold(
// err io(BadRequest(html.tournament.form(err, forms))),
// setup api.createTournament(setup, me) map { tour
// Redirect(routes.Tournament.show(tour.id))
// })
// }
// }
// }
def create = AuthBody { implicit ctx
implicit me OnePerWeek(me) {
IOResult {
implicit val req = ctx.body
forms.create.bindFromRequest.fold(
err io(BadRequest(html.team.form(err, forms.captchaCreate))),
setup api.create(setup, me) map { team
Redirect(routes.Team.show(team.id))
})
}
}
}
def mine = Auth { implicit ctx
me
IOk(api mine me map { html.team.mine(_) })
}
private def OnePerWeek[A <: Result](me: UserModel)(a: A)(implicit ctx: Context): Result = {
!Granter.superAdmin(me) &&
api.hasCreatedRecently(me).unsafePerformIO
} fold (Forbidden(views.html.team.createLimit()), a)
}

View file

@ -4,7 +4,7 @@ import lila._
import views._
import http.Context
import game.{ DbGame, Pov }
import security.{ Permission, Granter }
import security.Granter
import play.api.mvc._
import play.api.mvc.Results.Redirect
@ -15,5 +15,5 @@ trait TheftPrevention {
isTheft(pov).fold(Redirect(routes.Round.watcher(pov.gameId, pov.color.name)), ok)
protected def isTheft(pov: Pov)(implicit ctx: Context) =
pov.player.userId != ctx.userId && !Granter.option(Permission.SuperAdmin)(ctx.me)
pov.player.userId != ctx.userId && !Granter.superAdmin(ctx.me)
}

View file

@ -168,6 +168,7 @@ final class I18nKeys(translator: Translator) {
val nbMembers = new Key("nbMembers")
val allTeams = new Key("allTeams")
val newTeam = new Key("newTeam")
val myTeams = new Key("myTeams")
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)
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)
}

View file

@ -10,4 +10,8 @@ object Granter {
def option(permission: Permission)(user: Option[User]): Boolean =
user.fold(apply(permission), false)
def superAdmin(user: User): Boolean = apply(Permission.SuperAdmin)(user)
def superAdmin(user: Option[User]): Boolean = ~(user map superAdmin)
}

View file

@ -7,22 +7,32 @@ import play.api.data._
import play.api.data.Forms._
import play.api.data.validation.Constraints._
final class DataForm(captcher: Captcha) {
final class DataForm(
repo: TeamRepo,
captcher: Captcha) {
import lila.core.Form._
val create = Form(mapping(
"name" -> text(minLength = 3, maxLength = 60),
"location" -> optional(text(minLength = 3, maxLength = 80)),
"description" -> text(minLength = 60, maxLength = 1000),
"gameId" -> nonEmptyText,
"move" -> nonEmptyText
"description" -> text(minLength = 30, maxLength = 2000),
"gameId" -> text,
"move" -> text
)(TeamSetup.apply)(TeamSetup.unapply)
.verifying("This team already exists", d !teamExists(d))
.verifying(
"Not a checkmate",
data captcher get data.gameId valid data.move.trim.toLowerCase
)
)
def createWithCaptcha = create -> captchaCreate
def captchaCreate: Captcha.Challenge = captcher.create
private def teamExists(setup: TeamSetup) =
repo.exists(Team nameToId setup.name).unsafePerformIO
}
private[team] case class TeamSetup(

View file

@ -27,7 +27,7 @@ object Team {
location: Option[String],
description: String,
createdBy: User): Team = new Team(
id = templating.StringHelper.slugify(name),
id = nameToId(name),
name = name,
location = location,
description = description,
@ -35,4 +35,6 @@ object Team {
nbMembers = 1,
createdAt = DateTime.now,
createdBy = createdBy.id)
def nameToId(name: String) = templating.StringHelper slugify name
}

View file

@ -3,11 +3,16 @@ package team
import scalaz.effects._
import com.github.ornicar.paginator._
import org.scala_tools.time.Imports._
import user.User
final class TeamApi(
repo: TeamRepo,
maxPerPage: Int) {
val creationPeriod = 1 week
def popular(page: Int): Paginator[Team] = Paginator(
SalatAdapter(
dao = repo,
@ -16,4 +21,17 @@ final class TeamApi(
currentPage = page,
maxPerPage = maxPerPage
) | popular(1)
def mine(me: User): List[Team] = repo byUser user.id
def create(setup: TeamSetup, me: User): IO[Team] = Team(
name = setup.name,
location = setup.location,
description = setup.description,
createdBy = me) |> { team
repo saveIO team inject team
}
def hasCreatedRecently(me: User): IO[Boolean] =
repo.userHasCreatedSince(me.id, creationPeriod)
}

View file

@ -19,5 +19,5 @@ final class TeamEnv(
repo = repo,
maxPerPage = TeamPaginatorMaxPerPage)
lazy val forms = new DataForm(captcha)
lazy val forms = new DataForm(repo, captcha)
}

View file

@ -6,7 +6,7 @@ import com.novus.salat.dao._
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.query.Imports._
import scalaz.effects._
import org.joda.time.DateTime
import org.joda.time.{ DateTime, Period }
import org.scala_tools.time.Imports._
import user.User
@ -33,6 +33,15 @@ final class TeamRepo(collection: MongoCollection)
remove(selectId(team.id))
}
def exists(id: String): IO[Boolean] = byId(id) map (_.nonEmpty)
def userHasCreatedSince(userId: String, duration: Period): IO[Boolean] = io {
collection.find(
("createdAt" $gt (DateTime.now - duration)) +
("createdBy" -> userId)
).limit(1).size > 0
}
def userQuery(user: User) = DBObject("members.id" -> user.id)
def selectId(id: String) = DBObject("_id" -> id)

View file

@ -18,8 +18,11 @@ trait StringHelper {
slug.toLowerCase
}
def shorten(text: String, length: Int): String =
text.replace("\n", " ") take length
def shorten(text: String, length: Int, sep: String = " [...]"): String = {
val t = text.replace("\n", " ")
if (t.size > (length + sep.size)) (t take length) ++ sep
else t
}
def shortenWithBr(text: String, length: Int) = Html {
nl2br(escape(text).take(length))

View file

@ -36,6 +36,11 @@ case class User(
def canMessage = !muted
def canCreateTeam =
!isChatBan &&
nbGames >= 3 &&
createdAt < (DateTime.now - 3.hours)
def disabled = !enabled
def usernameWithElo = "%s (%d)".format(username, elo)

View file

@ -11,7 +11,6 @@
</style>
<div class="content_box">
<h1>@title</h1>
<br /><br />
<table class="datatable"><tbody>
@logs.map { log =>
<tr>

View file

@ -0,0 +1,19 @@
@(title: String)(message: Html)(implicit ctx: Context)
@site.layout(title = title) {
<div class="content_box small_box">
<div class="head">
<h1>@title</h1>
</div>
<br />
<br />
<p>
@message
</p>
<br />
<script>
document.write('<a href="' + document.referrer + '">Go Back</a>');
</script>
</div>
}

View file

@ -1,9 +1,5 @@
@()(implicit ctx: Context)
@site.layout(
title = "No engine area") {
<div class="content_box small_box">
Sorry, engines are not allowed in this area.
</div>
@site.message("No engine area") {
Sorry, engines are not allowed in this area.
}

View file

@ -0,0 +1,5 @@
@()(implicit ctx: Context)
@site.message("Cannot create a team") {
You have already created a team this week.
}

View file

@ -1,7 +1,8 @@
@(form: Form[_], captcha: lila.site.Captcha.Challenge)(implicit ctx: Context)
@team.layout(
title = "New Team") {
title = "New Team",
currentTab = "form".some) {
<div id="team">
<div class="content_box team_box">
<h1>@trans.newTeam()</h1>
@ -25,8 +26,8 @@ title = "New Team") {
}
</label>
</section>
@base.captcha(form("team")("move"), form("team")("gameId"), captcha)
@errMsg(form("team"))
@base.captcha(form("move"), form("gameId"), captcha)
@errMsg(form)
<button class="submit button" type="submit">@trans.newTeam()</button>
<a href="@routes.Team.home(1)" style="margin-left:20px">Cancel</a>
</form>

View file

@ -1,30 +1,7 @@
@(teams: Paginator[lila.team.Team])(implicit ctx: Context)
@team.layout(
title = trans.teams.str()) {
<div class="head">
<h1>@trans.teams()</h1>
</div>
<table>
<tbody class="infinitescroll">
@if(teams.hasToPaginate) {
<tr><th class="pager none">
<a href="@routes.Team.home(teams.nextPage | 1)">Next</a>
</th></tr>
}
@teams.currentPageResults.map { team =>
<tr class="paginated_element">
<td class="subject">
<a href="@routes.Team.show(team.id)">@team.name</a>
@autoLink(team.description)
</td>
<td class="info">
<p>@trans.nbMembers(team.nbMembers)</p>
</td>
</tr>
}
</tbody>
</tbody>
</table>
}
@team.list(
name = trans.teams.str(),
teams = teams,
next = teams.nextPage map { n => routes.Team.home(n) },
tab = "all")

View file

@ -1,14 +1,23 @@
@(title: String)(body: Html)(implicit ctx: Context)
@(title: String, currentTab: Option[String] = None, moreJs: Html = Html(""))(body: Html)(implicit ctx: Context)
@moreCss = {
@cssTag("team.css")
}
@goodies = {
<div class="sidebar">
<a href="@routes.Team.home(1)">@trans.allTeams()</a>
@if(ctx.me.fold(_.canMessage, false)) {
<a href="@routes.Team.form()">@trans.newTeam()</a>
<div class="side_menu">
@defining(~currentTab) { tab =>
<a class="@tab.active("all")" href="@routes.Team.home()">
@trans.allTeams()
</a>
@if(ctx.me.fold(_.canCreateTeam, false)) {
<a class="@tab.active("mine")" href="@routes.Team.mine()">
@trans.myTeams()
</a>
<a class="@tab.active("form")" href="@routes.Team.form()">
@trans.newTeam()
</a>
}
}
</div>
}
@ -16,6 +25,7 @@
@base.layout(
title = title,
moreCss = moreCss,
moreJs = moreJs,
goodies = goodies.some,
active = siteMenu.team.some) {
@body

View file

@ -0,0 +1,47 @@
@(name: String, teams: Paginator[lila.team.Team], next: Option[Call], tab: String)(implicit ctx: Context)
@title = @{ "%s - page %d".format(name, teams.currentPage) }
@moreJs = {
@jsTag("vendor/jquery.infinitescroll.min.js")
}
@team.layout(
title = title,
currentTab = tab.some,
moreJs = moreJs) {
<div id="team" class="content_box team_box no_padding">
<h1>@name</h1>
<table class="slist">
@if(teams.nbResults > 0) {
<tbody class="infinitescroll">
@next.map { n =>
<div class="pager none"><a href="@n">Next</a></div>
}.getOrElse {
<div class="none"></div>
}
@teams.currentPageResults.map { team =>
<tr class="paginated_element">
<td class="subject">
<a class="team-name" href="@routes.Team.show(team.id)">@team.name</a>
@shorten(team.description, 200)
</td>
<td class="info">
<p>@trans.nbMembers(team.nbMembers)</p>
</td>
</tr>
}
</tbody>
} else {
<tbody>
<tr>
<td colspan="2">
<br />
No team found
</td>
</tr>
</tbody>
}
</table>
</div>
}

View file

@ -0,0 +1,7 @@
@(teams: Paginator[lila.team.Team])(implicit ctx: Context)
@team.list(
name = trans.teams.str(),
teams = teams,
next = teams.nextPage map { n => routes.Team.home(n) },
tab = "all")

View file

@ -0,0 +1,11 @@
@(active: String)(implicit ctx: Context)
<a class="@active.active("all")" href="@routes.Team.home()">
@trans.allTeams()
</a>
<a class="@active.active("form")" href="@routes.Team.form()">
@trans.newTeam()
</a>
<a class="@active.active("mine")" href="@routes.Team.mine()">
@trans.myTeams
</a>

View file

@ -1,8 +1,8 @@
@(createds: List[lila.tournament.Created], starteds: List[lila.tournament.Started], finisheds: List[lila.tournament.Finished])(implicit ctx: Context)
<div class="content_box tournament_box">
<div class="content_box tournament_box no_padding">
<h1>Tournaments</h1>
<table class="data all_tournaments">
<table class="slist all_tournaments">
<thead>
<tr>
<th class="large">Open tournaments</th>

View file

@ -144,3 +144,4 @@ teams=Teams
nbMembers=%s members
allTeams=All teams
newTeam=New team
myTeams=My teams

View file

@ -54,6 +54,7 @@ GET /tournament/faq controllers.Tournament.faq
GET /team controllers.Team.home(page: Int ?= 1)
GET /team/new controllers.Team.form
POST /team/new controllers.Team.create
POST /team/me controllers.Team.mine
GET /team/:id controllers.Team.show(id: String)
# Analyse

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -166,6 +166,11 @@ div.content_box.small_box {
div.content_box.no_padding {
padding: 0;
}
div.content_box.no_padding h1 {
display: block;
padding: 20px 25px;
}
.content_box_title {
padding: 20px 25px 0 25px;
}
@ -220,6 +225,9 @@ div.content_box .lichess_title {
margin-bottom: 0.5em;
}
div.content_box table.datatable {
margin-top: 20px;
}
div.content_box table.datatable td {
padding: 5px 10px 5px 10px;
}
@ -227,6 +235,40 @@ div.content_box table.datatable tr:nth-child(odd) {
background: #f7f7f7;
}
table.slist {
width: 100%;
border-bottom: 1px solid #d4d4d4;
}
table.slist thead th {
border-top: 1px solid #d4d4d4;
padding: 0.4em 0.6em;
}
table.slist thead th.large {
font-size: 1.4em;
}
table.slist td {
padding: 0.6em;
font-size: 1.2em;
}
table.slist td .label {
font-family: monospace;
font-size: 10px;
}
table.slist .s16 {
display: block;
height: 16px;
padding: 0 0 0 20px;
}
table.slist tbody tr:nth-child(even) {
background: #f4f4f4;
}
table.slist td:first-child,
table.slist th:first-child {
padding-left: 25px;
}
div.lichess_goodies div.box {
margin-top: 1em;
margin-left: -30px;
@ -586,7 +628,7 @@ div.content_box_top,
div.hooks tr,
a.translation_call,
div.locale_menu a,
#tournament table.data thead {
table.data thead {
color: #666;
background: #fafafa;
background: -moz-linear-gradient(center top , #fafafa, #e0e0e0) repeat scroll 0 0 #fafafa;

View file

@ -87,8 +87,8 @@ body.dark div.adv_chart,
body.dark #top .dropdown,
body.dark div.lichess_overboard p.explanations,
body.dark div.search_status,
body.dark #tournament table.data,
body.dark #tournament table.data thead th,
body.dark table.slist,
body.dark table.slist thead th,
body.dark div.notifications > div,
body.dark form.wide input[type="text"],
body.dark form.wide textarea
@ -211,7 +211,7 @@ body.dark #lichess_message tr:nth-child(even),
body.dark div.user_show .elo_with_me,
body.dark div.content_box_inter,
body.dark #GameText tr:nth-child(even),
body.dark #tournament table.data tbody tr:nth-child(even),
body.dark table.slist tbody tr:nth-child(even),
body.dark table.translations tbody tr:nth-child(even),
body.dark form.translation_form div.message:nth-child(even),
body.dark div.content_box table.datatable tr:nth-child(odd),
@ -286,7 +286,7 @@ body.dark div.hooks tr,
body.dark a.translation_call,
body.dark div.notification,
body.dark div.locale_menu a,
body.dark #tournament table.data thead
body.dark table.slist thead
{
background: #3a3a3a;
color: #b0b0b0;

View file

@ -36,3 +36,13 @@ form.new-team p.error {
margin-bottom: 10px;
color: red;
}
#team table a.team-name {
font-size: 1.3em;
display: block;
margin-bottom: 4px;
}
#team table .info {
font-size: 0.9em;
white-space: nowrap;
}

View file

@ -1,11 +1,3 @@
#tournament div.tournament_box {
padding: 0;
}
#tournament div.tournament_box h1 {
display: block;
padding: 20px 25px;
}
#tournament .title_tag {
float: right;
font-size: 20px;
@ -33,51 +25,18 @@
font-weight: bold;
}
#tournament table.data {
width: 100%;
border-bottom: 1px solid #d4d4d4;
}
#tournament table.data thead th {
border-top: 1px solid #d4d4d4;
padding: 0.4em 0.6em;
}
#tournament table.data thead th.large {
font-size: 1.4em;
}
#tournament table.data td {
padding: 0.6em;
font-size: 1.2em;
}
#tournament table.data .create td {
#tournament table.slist .create td {
padding: 0.6em 1.2em 0.6em 0.6em;
text-align: center;
}
#tournament table.data td .label {
font-family: monospace;
font-size: 10px;
}
#tournament table.data .s16 {
display: block;
height: 16px;
padding: 0 0 0 20px;
}
#tournament table.data tbody tr:nth-child(even) {
background: #f4f4f4;
}
#tournament table.data td:first-child,
#tournament table.data th:first-child {
padding-left: 25px;
}
#tournament table.data span.rank {
#tournament table.slist span.rank {
font-weight: bold;
padding-right: 20px;
background: transparent url(../images/s16.png) top right no-repeat;
background-position: right 900px;
}
#tournament table.data tbody span.withdraw {
#tournament table.slist tbody span.withdraw {
background-position: right -320px;
}
#tournament table.data tbody span.winner {