team battle WIP

team-tournament
Thibault Duplessis 2019-10-02 12:20:44 +02:00
parent 0984d83d47
commit f0e9a09422
18 changed files with 172 additions and 38 deletions

View File

@ -16,6 +16,8 @@ object Simul extends LilaController {
private def env = Env.simul
private def forms = lila.simul.SimulForm
import Team.teamsIBelongTo
private def simulNotFound(implicit ctx: Context) = NotFound(html.simul.bits.notFound())
val home = Open { implicit ctx =>
@ -154,7 +156,4 @@ object Simul extends LilaController {
case Some(simul) if ctx.userId.exists(simul.hostId ==) => fuccess(f(simul))
case _ => fuccess(Unauthorized)
}
private def teamsIBelongTo(me: lila.user.User): Fu[TeamIdsWithNames] =
Env.team.api.mine(me) map { _.map(t => t._id -> t.name) }
}

View File

@ -3,6 +3,7 @@ package controllers
import lila.api.Context
import lila.app._
import lila.common.{ HTTPRequest, MaxPerSecond }
import lila.hub.lightTeam._
import lila.security.Granter
import lila.team.{ Joined, Motivate, Team => TeamModel, TeamRepo, MemberRepo }
import lila.user.{ User => UserModel }
@ -229,4 +230,7 @@ object Team extends LilaController {
private def Owner(team: TeamModel)(a: => Fu[Result])(implicit ctx: Context): Fu[Result] =
if (ctx.me.??(me => team.isCreator(me.id) || isGranted(_.ManageTeam))) a
else renderTeam(team) map { Forbidden(_) }
private[controllers] def teamsIBelongTo(me: lila.user.User): Fu[List[LightTeam]] =
api mine me map { _.map(_.light) }
}

View File

@ -18,6 +18,8 @@ object Tournament extends LilaController {
private def env = Env.tournament
private def repo = TournamentRepo
import Team.teamsIBelongTo
private def tournamentNotFound(implicit ctx: Context) = NotFound(html.tournament.bits.notFound())
private[controllers] val upcomingCache = Env.memo.asyncCache.single[(VisibleTournaments, List[Tour])](
@ -172,8 +174,18 @@ object Tournament extends LilaController {
def form = Auth { implicit ctx => me =>
NoLameOrBot {
teamsIBelongTo(me) flatMap { teams =>
Ok(html.tournament.form(env.forms(me), env.forms, me, teams)).fuccess
teamsIBelongTo(me) map { teams =>
Ok(html.tournament.form(env.forms(me), env.forms, me, teams))
}
}
}
def formTeamBattle(teamId: String) = Auth { implicit ctx => me =>
NoLameOrBot {
Env.team.api.owns(teamId, me.id) map {
_ ?? {
Ok(html.tournament.form(env.forms(me, teamId.some), env.forms, me, Nil))
}
}
}
}
@ -210,9 +222,15 @@ object Tournament extends LilaController {
setup.password.isDefined) 1 else 4
CreateLimitPerUser(me.id, cost = cost) {
CreateLimitPerIP(HTTPRequest lastRemoteAddress ctx.req, cost = cost) {
env.api.createTournament(setup, me, teams, getUserTeamIds) flatMap { tour =>
fuccess(Redirect(routes.Tournament.show(tour.id)))
}
env.api.createTournament(
setup,
me,
teams,
getUserTeamIds,
Env.team.api.filterExistingIds
) flatMap { tour =>
fuccess(Redirect(routes.Tournament.show(tour.id)))
}
}(rateLimited)
}(rateLimited)
}
@ -232,7 +250,7 @@ object Tournament extends LilaController {
env.forms(me).bindFromRequest.fold(
jsonFormErrorDefaultLang,
setup => teamsIBelongTo(me) flatMap { teams =>
env.api.createTournament(setup, me, teams, getUserTeamIds) flatMap { tour =>
env.api.createTournament(setup, me, teams, getUserTeamIds, Env.team.api.filterExistingIds) flatMap { tour =>
Env.tournament.jsonView(tour, none, none, getUserTeamIds, none, none, partial = false, lila.i18n.defaultLang)
}
} map { Ok(_) }
@ -294,8 +312,6 @@ object Tournament extends LilaController {
expireAfter = _.ExpireAfterWrite(15.seconds)
)
private def getUserTeamIds(user: lila.user.User): Fu[TeamIdList] =
private def getUserTeamIds(user: lila.user.User): Fu[List[TeamId]] =
Env.team.cached.teamIdsList(user.id)
private def teamsIBelongTo(me: lila.user.User): Fu[TeamIdsWithNames] =
Env.team.api.mine(me) map { _.map(t => t._id -> t.name) }
}

View File

@ -11,7 +11,7 @@ import controllers.routes
object form {
def apply(form: Form[lila.simul.SimulForm.Setup], teams: lila.hub.lightTeam.TeamIdsWithNames)(implicit ctx: Context) = {
def apply(form: Form[lila.simul.SimulForm.Setup], teams: List[lila.hub.lightTeam.LightTeam])(implicit ctx: Context) = {
import lila.simul.SimulForm._
@ -42,7 +42,9 @@ object form {
form3.group(form("color"), raw("Host color for each game"), half = true)(form3.select(_, colorChoices))
),
(teams.size > 0) ?? {
form3.group(form("team"), raw("Only members of team"), half = false)(form3.select(_, List(("", "No Restriction")) ::: teams))
form3.group(form("team"), raw("Only members of team"), half = false)(
form3.select(_, List(("", "No Restriction")) ::: teams.map(_.pair))
)
},
form3.group(form("text"), raw("Simul description"), help = frag("Anything you want to tell the participants?").some)(form3.textarea(_)(rows := 10)),
form3.actions(

View File

@ -65,8 +65,10 @@ object show {
postForm(cls := "quit", action := routes.Team.quit(t.id))(
submitButton(cls := "button button-empty button-red confirm")(trans.quitTeam.txt())
),
(info.createdByMe || isGranted(_.Admin)) option
a(href := routes.Team.edit(t.id), cls := "button button-empty text", dataIcon := "%")(trans.settings())
(info.createdByMe || isGranted(_.Admin)) option frag(
a(href := routes.Team.edit(t.id), cls := "button button-empty text", dataIcon := "%")(trans.settings()),
a(href := routes.Tournament.formTeamBattle(t.id), cls := "button button-empty text", dataIcon := "g")("Team Battle")
)
),
NotForKids {
st.section(cls := "team-show__forum")(

View File

@ -6,14 +6,14 @@ import play.api.data.{ Field, Form }
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.user.User
import lila.tournament.{ Condition, DataForm }
import lila.user.User
import controllers.routes
object form {
def apply(form: Form[_], config: DataForm, me: User, teams: lila.hub.lightTeam.TeamIdsWithNames)(implicit ctx: Context) = views.html.base.layout(
def apply(form: Form[_], config: DataForm, me: User, teams: List[lila.hub.lightTeam.LightTeam])(implicit ctx: Context) = views.html.base.layout(
title = trans.newTournament.txt(),
moreCss = cssTag("tournament.form"),
moreJs = frag(
@ -62,12 +62,14 @@ object form {
a(cls := "show")(trans.showAdvancedSettings())
),
div(cls := "form")(
form3.group(form("password"), trans.password(), help = raw("Make the tournament private, and restrict access with a password").some)(form3.input(_)),
form("teamBattle")("teams").value.isEmpty option
form3.group(form("password"), trans.password(), help = raw("Make the tournament private, and restrict access with a password").some)(form3.input(_)),
condition(form, auto = true, teams = teams),
input(tpe := "hidden", name := form("berserkable").name, value := "false"), // hack allow disabling berserk
form3.group(form("startDate"), raw("Custom start date"), help = raw("""This overrides the "Time before tournament starts" setting""").some)(form3.flatpickr(_))
)
),
form3.hidden(form("teamBattle")("teams")),
form3.actions(
a(href := routes.Tournament.home())(trans.cancel()),
form3.submit(trans.createANewTournament(), icon = "g".some)
@ -81,7 +83,7 @@ object form {
if (auto) form3.hidden(field) else visible(field)
)
def condition(form: Form[_], auto: Boolean, teams: lila.hub.lightTeam.TeamIdsWithNames)(implicit ctx: Context) = frag(
def condition(form: Form[_], auto: Boolean, teams: List[lila.hub.lightTeam.LightTeam])(implicit ctx: Context) = frag(
form3.split(
form3.group(form("conditions.nbRatedGame.nb"), raw("Minimum rated games"), half = true)(form3.select(_, Condition.DataForm.nbRatedGameChoices)),
autoField(auto, form("conditions.nbRatedGame.perf")) { field =>
@ -107,7 +109,9 @@ object form {
form3.checkbox(form("berserkable"), raw("Allow Berserk"), help = raw("Let players halve their clock time to gain an extra point").some, half = true)
),
(auto && teams.size > 0) ?? {
form3.group(form("conditions.teamMember.teamId"), raw("Only members of team"), half = false)(form3.select(_, List(("", "No Restriction")) ::: teams))
form3.group(form("conditions.teamMember.teamId"), raw("Only members of team"), half = false)(
form3.select(_, List(("", "No Restriction")) ::: teams.map(_.pair))
)
}
)

View File

@ -213,6 +213,7 @@ GET /whats-next/$fullId<\w{12}> controllers.Round.whatsNext(ful
GET /tournament controllers.Tournament.home(page: Int ?= 1)
GET /tournament/featured controllers.Tournament.featured
GET /tournament/new controllers.Tournament.form
GET /tournament/new-team-battle controllers.Tournament.formTeamBattle(teamId: String)
POST /tournament/new controllers.Tournament.create
GET /tournament/calendar controllers.Tournament.calendar
GET /tournament/$id<\w{8}> controllers.Tournament.show(id: String)

View File

@ -3,6 +3,7 @@ package lila.hub
package object lightTeam {
type TeamId = String
type TeamName = String
type TeamIdList = List[TeamId]
type TeamIdsWithNames = List[(TeamId, TeamName)]
case class LightTeam(id: TeamId, name: TeamName) {
def pair = id -> name
}
}

View File

@ -24,6 +24,8 @@ case class Team(
def disabled = !enabled
def isCreator(user: String) = user == createdBy
def light = lila.hub.lightTeam.LightTeam(_id, name)
}
object Team {

View File

@ -197,10 +197,13 @@ final class TeamApi(
cached.teamIds(userId) map (_ contains teamId)
def owns(teamId: Team.ID, userId: User.ID): Fu[Boolean] =
TeamRepo ownerOf teamId map (Some(userId) ==)
TeamRepo ownerOf teamId map (_ has userId)
def teamName(teamId: Team.ID): Option[String] = cached.name(teamId)
def filterExistingIds(ids: Set[String]): Fu[Set[Team.ID]] =
coll.team.distinct[Team.ID, Set]("_id", Some("_id" $in ids))
def nbRequests(teamId: Team.ID) = cached.nbRequests get teamId
def recomputeNbMembers =

View File

@ -98,7 +98,7 @@ object Condition {
case class TeamMember(teamId: TeamId, teamName: TeamName) extends Condition {
def name(lang: Lang) = I18nKeys.mustBeInTeam.literalTxtTo(lang, List(teamName))
def apply(user: User, getUserTeamIds: User => Fu[TeamIdList]) =
def apply(user: User, getUserTeamIds: User => Fu[List[TeamId]]) =
getUserTeamIds(user) map { userTeamIds =>
if (userTeamIds contains teamId) Accepted
else Refused { lang => I18nKeys.youAreNotInTeam.literalTxtTo(lang, List(teamName)) }
@ -119,7 +119,7 @@ object Condition {
def ifNonEmpty = list.nonEmpty option this
def withVerdicts(getMaxRating: GetMaxRating)(user: User, getUserTeamIds: User => Fu[TeamIdList]): Fu[All.WithVerdicts] =
def withVerdicts(getMaxRating: GetMaxRating)(user: User, getUserTeamIds: User => Fu[List[TeamId]]): Fu[All.WithVerdicts] =
list.map {
case c: MaxRating => c(getMaxRating)(user) map c.withVerdict
case c: FlatCond => fuccess(c withVerdict c(user))
@ -153,11 +153,11 @@ object Condition {
}
final class Verify(historyApi: lila.history.HistoryApi) {
def apply(all: All, user: User, getUserTeamIds: User => Fu[TeamIdList]): Fu[All.WithVerdicts] = {
def apply(all: All, user: User, getUserTeamIds: User => Fu[List[TeamId]]): Fu[All.WithVerdicts] = {
val getMaxRating: GetMaxRating = perf => historyApi.lastWeekTopRating(user, perf)
all.withVerdicts(getMaxRating)(user, getUserTeamIds)
}
def canEnter(user: User, getUserTeamIds: User => Fu[TeamIdList])(tour: Tournament): Fu[Boolean] =
def canEnter(user: User, getUserTeamIds: User => Fu[List[TeamId]])(tour: Tournament): Fu[Boolean] =
apply(tour.conditions, user, getUserTeamIds).map(_.accepted)
}

View File

@ -8,7 +8,7 @@ import play.api.data.validation.Constraints
import chess.Mode
import chess.StartingPosition
import lila.common.Form._
import lila.common.Form.ISODateTime._
import lila.hub.lightTeam._
import lila.user.User
final class DataForm {
@ -16,7 +16,7 @@ final class DataForm {
import DataForm._
import UTCDate._
def apply(user: User) = create fill TournamentSetup(
def apply(user: User, teamBattleId: Option[TeamId] = None) = create fill TournamentSetup(
name = canPickName(user) option user.titleUsername,
clockTime = clockTimeDefault,
clockIncrement = clockIncrementDefault,
@ -29,6 +29,7 @@ final class DataForm {
mode = none,
rated = true.some,
conditions = Condition.DataForm.AllSetup.default,
teamBattle = teamBattleId map TeamBattle.DataForm.Setup.apply,
berserkable = true.some
)
@ -54,6 +55,7 @@ final class DataForm {
"rated" -> optional(boolean),
"password" -> optional(nonEmptyText),
"conditions" -> Condition.DataForm.all,
"teamBattle" -> optional(TeamBattle.DataForm.form),
"berserkable" -> optional(boolean)
)(TournamentSetup.apply)(TournamentSetup.unapply)
.verifying("Invalid clock", _.validClock)
@ -119,6 +121,7 @@ private[tournament] case class TournamentSetup(
rated: Option[Boolean],
password: Option[String],
conditions: Condition.DataForm.AllSetup,
teamBattle: Option[TeamBattle.DataForm.Setup],
berserkable: Option[Boolean]
) {

View File

@ -38,7 +38,7 @@ final class JsonView(
tour: Tournament,
page: Option[Int],
me: Option[User],
getUserTeamIds: User => Fu[TeamIdList],
getUserTeamIds: User => Fu[List[TeamId]],
playerInfoExt: Option[PlayerInfoExt],
socketVersion: Option[SocketVersion],
partial: Boolean,

View File

@ -0,0 +1,26 @@
package lila.tournament
import lila.hub.lightTeam._
case class TeamBattle(
teams: Set[TeamId]
)
object TeamBattle {
object DataForm {
import play.api.data.Forms._
import lila.common.Form._
val form = mapping(
"teams" -> nonEmptyText
)(Setup.apply)(Setup.unapply)
case class Setup(
teams: String
) {
def potentialTeamIds: Set[String] =
teams.lines.mkString(" ").split(" ").filter(_.nonEmpty).toSet
}
}
}

View File

@ -21,6 +21,7 @@ case class Tournament(
mode: Mode,
password: Option[String] = None,
conditions: Condition.All,
teamBattle: Option[TeamBattle] = None,
noBerserk: Boolean = false,
schedule: Option[Schedule],
nbPlayers: Int,

View File

@ -46,8 +46,14 @@ final class TournamentApi(
private val bus = system.lilaBus
def createTournament(setup: TournamentSetup, me: User, myTeams: List[(String, String)], getUserTeamIds: User => Fu[TeamIdList]): Fu[Tournament] = {
val tour = Tournament.make(
def createTournament(
setup: TournamentSetup,
me: User,
myTeams: List[LightTeam],
getUserTeamIds: User => Fu[List[TeamId]],
filterExistingTeamIds: Set[TeamId] => Fu[Set[TeamId]]
): Fu[Tournament] = {
Tournament.make(
by = Right(me),
name = DataForm.canPickName(me) ?? setup.name,
clock = setup.clockConfig,
@ -62,9 +68,16 @@ final class TournamentApi(
berserkable = setup.berserkable | true
) |> { tour =>
tour.perfType.fold(tour) { perfType =>
tour.copy(conditions = setup.conditions.convert(perfType, myTeams toMap))
tour.copy(conditions = setup.conditions.convert(perfType, myTeams.map(_.pair)(collection.breakOut)))
}
} |> { tour =>
setup.teamBattle.fold(fuccess(tour)) { battle =>
filterExistingTeamIds(battle.potentialTeamIds) map { teamIds =>
tour.copy(teamBattle = teamIds.nonEmpty option TeamBattle(teamIds))
}
}
}
} flatMap { tour =>
if (tour.name != me.titleUsername && lila.common.LameName.anyNameButLichessIsOk(tour.name))
bus.publish(lila.hub.actorApi.slack.TournamentName(me.username, tour.id, tour.name), 'slack)
logger.info(s"Create $tour")
@ -191,7 +204,7 @@ final class TournamentApi(
}
}
def verdicts(tour: Tournament, me: Option[User], getUserTeamIds: User => Fu[TeamIdList]): Fu[Condition.All.WithVerdicts] = me match {
def verdicts(tour: Tournament, me: Option[User], getUserTeamIds: User => Fu[List[TeamId]]): Fu[Condition.All.WithVerdicts] = me match {
case None => fuccess(tour.conditions.accepted)
case Some(user) => verify(tour.conditions, user, getUserTeamIds)
}
@ -200,7 +213,7 @@ final class TournamentApi(
tourId: Tournament.ID,
me: User,
p: Option[String],
getUserTeamIds: User => Fu[TeamIdList],
getUserTeamIds: User => Fu[List[TeamId]],
promise: Option[Promise[Boolean]]
): Unit = Sequencing(tourId)(TournamentRepo.enterableById) { tour =>
if (tour.password == p) {
@ -226,7 +239,7 @@ final class TournamentApi(
def joinWithResult(
tourId: Tournament.ID,
me: User, p: Option[String],
getUserTeamIds: User => Fu[TeamIdList]
getUserTeamIds: User => Fu[List[TeamId]]
): Fu[Boolean] = {
val promise = Promise[Boolean]
join(tourId, me, p, getUserTeamIds, promise.some)

View File

@ -20,4 +20,62 @@ $(function() {
altInput: true,
altFormat: 'Y-m-d h:i K'
});
// if (topicId) lichess.loadScript('vendor/textcomplete.min.js').then(function() {
// var searchCandidates = function(term, candidateUsers) {
// return candidateUsers.filter(function(user) {
// return user.toLowerCase().startsWith(term.toLowerCase());
// });
// };
// // We only ask the server for the thread participants once the user has clicked the text box as most hits to the
// // forums will be only to read the thread. So the 'thread participants' starts out empty until the post text area
// // is focused.
// var threadParticipants = $.ajax({
// url: "/forum/participants/" + topicId
// });
// var textcomplete = new Textcomplete(new Textcomplete.editors.Textarea(textarea));
// textcomplete.register([{
// match: /(^|\s)@(|[a-zA-Z_-][\w-]{0,19})$/,
// search: function(term, callback) {
// // Initially we only autocomplete by participants in the thread. As the user types more,
// // we can autocomplete against all users on the site.
// threadParticipants.then(function(participants) {
// var forumParticipantCandidates = searchCandidates(term, participants);
// if (forumParticipantCandidates.length != 0) {
// // We always prefer a match on the forum thread partcipants' usernames
// callback(forumParticipantCandidates);
// }
// else if (term.length >= 3) {
// // We fall back to every site user after 3 letters of the username have been entered
// // and there are no matches in the forum thread participants
// $.ajax({
// url: "/player/autocomplete",
// data: {
// term: term
// },
// success: function(candidateUsers) {
// callback(searchCandidates(term, candidateUsers));
// },
// cache: true
// });
// } else {
// callback([]);
// }
// });
// },
// replace: function(mention) {
// return '$1@' + mention + ' ';
// }
// }], {
// placement: 'top',
// appendTo: '#lichess_forum'
// });
// });
// });
});

File diff suppressed because one or more lines are too long