tournament manager
parent
b7733b9aee
commit
e7aed405ea
|
@ -0,0 +1,58 @@
|
|||
package controllers
|
||||
|
||||
import play.api.data.Form
|
||||
import play.api.libs.json._
|
||||
import play.api.mvc._
|
||||
|
||||
import lila.api.Context
|
||||
import lila.app._
|
||||
import lila.common.HTTPRequest
|
||||
import lila.game.{ Pov, GameRepo }
|
||||
import lila.tournament.{ System, TournamentRepo, PairingRepo, Tournament => Tourney, VisibleTournaments }
|
||||
import lila.user.UserRepo
|
||||
import views._
|
||||
|
||||
object TournamentCrud extends LilaController {
|
||||
|
||||
private def env = Env.tournament
|
||||
private def crud = Env.tournament.crudApi
|
||||
|
||||
def index = Secure(_.ManageTournament) { implicit ctx =>
|
||||
me =>
|
||||
crud.list map { tours =>
|
||||
html.tournament.crud.index(tours)
|
||||
}
|
||||
}
|
||||
|
||||
def edit(id: String) = Secure(_.ManageTournament) { implicit ctx =>
|
||||
me =>
|
||||
OptionOk(crud one id) { tour =>
|
||||
html.tournament.crud.edit(tour, crud editForm tour)
|
||||
}
|
||||
}
|
||||
|
||||
def update(id: String) = SecureBody(_.ManageTournament) { implicit ctx =>
|
||||
me =>
|
||||
OptionFuResult(crud one id) { tour =>
|
||||
implicit val req = ctx.body
|
||||
crud.editForm(tour).bindFromRequest.fold(
|
||||
err => BadRequest(html.tournament.crud.edit(tour, err)).fuccess,
|
||||
data => crud.update(tour, data) inject Redirect(routes.TournamentCrud.index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def form = Secure(_.ManageTournament) { implicit ctx =>
|
||||
me =>
|
||||
Ok(html.tournament.crud.create(crud.createForm)).fuccess
|
||||
}
|
||||
|
||||
def create = SecureBody(_.ManageTournament) { implicit ctx =>
|
||||
me =>
|
||||
implicit val req = ctx.body
|
||||
crud.createForm.bindFromRequest.fold(
|
||||
err => BadRequest(html.tournament.crud.create(err)).fuccess,
|
||||
data => crud.create(data) inject Redirect(routes.TournamentCrud.index)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import scala.collection.mutable
|
|||
|
||||
import org.joda.time.format._
|
||||
import org.joda.time.format.ISODateTimeFormat
|
||||
import org.joda.time.{ Period, PeriodType, DurationFieldType, DateTime }
|
||||
import org.joda.time.{ Period, PeriodType, DurationFieldType, DateTime, DateTimeZone }
|
||||
import play.twirl.api.Html
|
||||
|
||||
import lila.api.Context
|
||||
|
@ -48,6 +48,12 @@ trait DateHelper { self: I18nHelper =>
|
|||
def showDateTime(date: DateTime)(implicit ctx: Context): String =
|
||||
dateTimeFormatter(ctx) print date
|
||||
|
||||
def showDateTimeZone(date: DateTime, zone: DateTimeZone)(implicit ctx: Context): String =
|
||||
dateTimeFormatter(ctx) print date.toDateTime(zone)
|
||||
|
||||
def showDateTimeUTC(date: DateTime)(implicit ctx: Context): String =
|
||||
showDateTimeZone(date, DateTimeZone.UTC)
|
||||
|
||||
def showDate(date: DateTime)(implicit ctx: Context): String =
|
||||
dateFormatter(ctx) print date
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
@(form: Form[_])(implicit ctx: Context)
|
||||
|
||||
@layout(title = "New tournament", active = "form") {
|
||||
<div class="tour_crud content_box small_box no_padding">
|
||||
<h1 class="lichess_title">New tournament</h1>
|
||||
<form class="content_box_content material form" action="@routes.TournamentCrud.create" method="POST">
|
||||
@inForm(form)
|
||||
</form>
|
||||
</div>
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
@(tour: lila.tournament.Tournament, form: Form[_])(implicit ctx: Context)
|
||||
|
||||
@layout(title = tour.fullName) {
|
||||
<div class="tour_crud content_box small_box no_padding">
|
||||
<h1 class="lichess_title">@tour.fullName</h1>
|
||||
<form class="content_box_content material form" action="@routes.TournamentCrud.update(tour.id)" method="POST">
|
||||
@inForm(form)
|
||||
</form>
|
||||
</div>
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
@(form: Form[_])(implicit ctx: Context)
|
||||
|
||||
@import lila.tournament.DataForm._
|
||||
@import lila.tournament.crud.CrudForm._
|
||||
|
||||
@group(field: play.api.data.Field, name: Html, half: Boolean = false)(html: Html) = {
|
||||
<div class="form-group@if(half){ half}@if(field.hasErrors){ has-error}">
|
||||
@html
|
||||
<label for="@field.id" class="control-label">@name</label>
|
||||
<i class="bar"></i>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
@group(form("date"), Html("Date <strong>yyyy-mm-dd</strong>"), half = true) {
|
||||
@base.input(form("date"))
|
||||
}
|
||||
<div class="form-group half">
|
||||
@group(form("dateHour"), Html("Hour <strong>UTC</strong>"), half = true) {
|
||||
@base.input(form("dateHour"))
|
||||
}
|
||||
@group(form("dateMinute"), Html("Minute <strong>UTC</strong>"), half = true) {
|
||||
@base.input(form("dateMinute"))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@group(form("name"), Html("Name")) {
|
||||
@base.input(form("name"))
|
||||
}
|
||||
<div>
|
||||
@group(form("homepageHours"), Html("Hours on homepage (0 to 24)"), half = true) {
|
||||
@base.input(form("homepageHours"))
|
||||
}
|
||||
@group(form("image"), Html("Custom icon"), half = true) {
|
||||
@base.select(form("image"), imageChoices)
|
||||
}
|
||||
@group(form("headline"), Html("Homepage headline")) {
|
||||
@base.input(form("headline"))
|
||||
}
|
||||
</div>
|
||||
@group(form("description"), Html("Full description")) {
|
||||
<textarea name="@form("description").name" id="@form("description").id">@form("description").value</textarea>
|
||||
}
|
||||
<div>
|
||||
@group(form("variant"), Html("Variant"), half = true) {
|
||||
@base.select(form("variant"), translatedVariantChoicesWithVariants.map(x => x._1 -> x._2))
|
||||
}
|
||||
@group(form("minutes"), Html("Duration in minutes"), half = true) {
|
||||
@base.input(form("minutes"))
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
@group(form("clockTime"), Html("Clock time"), half = true) {
|
||||
@base.select(form("clockTime"), clockTimeChoices)
|
||||
}
|
||||
@group(form("clockIncrement"), Html("Clock increment"), half = true) {
|
||||
@base.select(form("clockIncrement"), clockIncrementChoices)
|
||||
}
|
||||
</div>
|
||||
@group(form("mode"), Html("Mode")) {
|
||||
@base.select(form("mode"), translatedModeChoices.map(x => x._1 -> x._2))
|
||||
}
|
||||
|
||||
<div class="button-container">
|
||||
<button type="submit" class="submit button text" data-icon="E">Apply now</button>
|
||||
</div>
|
|
@ -0,0 +1,31 @@
|
|||
@(tours: List[lila.tournament.Tournament])(implicit ctx: Context)
|
||||
|
||||
@title = {Tournament manager}
|
||||
|
||||
@layout(title = title.body, active = "index") {
|
||||
<div class="tour_crud content_box no_padding">
|
||||
<h1 class="lichess_title">@title</h1>
|
||||
<table class="slist">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Variant</th>
|
||||
<th>Clock</th>
|
||||
<th>Duration</th>
|
||||
<th>UTC Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@tours.map { tour =>
|
||||
<tr>
|
||||
<td><a href="@routes.TournamentCrud.edit(tour.id)">@tour.fullName</a></td>
|
||||
<td>@tour.variant.name</td>
|
||||
<td>@tour.clock</td>
|
||||
<td>@{tour.minutes}m</td>
|
||||
<td>@showDateTimeUTC(tour.startsAt)</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
@(title: String, active: String = "")(body: Html)(implicit ctx: Context)
|
||||
|
||||
@menu = {
|
||||
<a class="@active.active("index")" href="@routes.TournamentCrud.index">Recent tournaments</a>
|
||||
<a class="@active.active("form")" href="@routes.TournamentCrud.form">Create a tournament</a>
|
||||
}
|
||||
|
||||
@moreCss = {
|
||||
@cssTag("material.form.css")
|
||||
@cssTag("tournament.crud.css")
|
||||
}
|
||||
@moreJs = {
|
||||
@jsTag("tournament.crud.js")
|
||||
}
|
||||
|
||||
@base.layout(
|
||||
title = title,
|
||||
menu = menu.some,
|
||||
moreCss = moreCss,
|
||||
moreJs = moreJs)(body)
|
|
@ -1,6 +1,7 @@
|
|||
@(form: Form[_], config: lila.tournament.DataForm)(implicit ctx: Context)
|
||||
|
||||
@import config._
|
||||
@import lila.tournament.DataForm._
|
||||
|
||||
@moreJs = {
|
||||
@jsTag("tournamentForm.js")
|
||||
|
@ -64,7 +65,7 @@ moreJs = moreJs) {
|
|||
</tr>
|
||||
<tr>
|
||||
<th><label for="@form("waitMinutes").id">@trans.timeBeforeTournamentStarts()</label></th>
|
||||
<td>@base.select(form("waitMinutes"), config.waitMinuteChoices)</td>
|
||||
<td>@base.select(form("waitMinutes"), waitMinuteChoices)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
|
@ -79,20 +80,20 @@ moreJs = moreJs) {
|
|||
</table>
|
||||
</form>
|
||||
<div class="none private_time">
|
||||
@base.select(form("clockTime"), config.clockTimePrivateChoices)
|
||||
@base.select(form("clockTime"), clockTimePrivateChoices)
|
||||
+
|
||||
@base.select(form("clockIncrement"), config.clockIncrementPrivateChoices)
|
||||
@base.select(form("clockIncrement"), clockIncrementPrivateChoices)
|
||||
</div>
|
||||
<div class="none public_time">
|
||||
@base.select(form("clockTime"), config.clockTimeChoices)
|
||||
@base.select(form("clockTime"), clockTimeChoices)
|
||||
+
|
||||
@base.select(form("clockIncrement"), config.clockIncrementChoices)
|
||||
@base.select(form("clockIncrement"), clockIncrementChoices)
|
||||
</div>
|
||||
<div class="none private_minutes">
|
||||
@base.select(form("minutes"), config.minutePrivateChoices)
|
||||
@base.select(form("minutes"), minutePrivateChoices)
|
||||
</div>
|
||||
<div class="none public_minutes">
|
||||
@base.select(form("minutes"), config.minuteChoices)
|
||||
@base.select(form("minutes"), minuteChoices)
|
||||
</div>
|
||||
</div>
|
||||
<div class="content_box small_box faq_box tournament_box">
|
||||
|
|
|
@ -166,6 +166,13 @@ GET /tournament/$id<\w{8}>/player/:user controllers.Tournament.player(id:
|
|||
POST /tournament/$id<\w{8}>/terminate controllers.Tournament.terminate(id: String)
|
||||
GET /tournament/help controllers.Tournament.help(system: Option[String] ?= None)
|
||||
|
||||
# Tournament CRUD
|
||||
GET /tournament/manager controllers.TournamentCrud.index
|
||||
GET /tournament/manager/$id<\w{8}> controllers.TournamentCrud.edit(id: String)
|
||||
POST /tournament/manager/$id<\w{8}> controllers.TournamentCrud.update(id: String)
|
||||
GET /tournament/manager/new controllers.TournamentCrud.form
|
||||
POST /tournament/manager controllers.TournamentCrud.create
|
||||
|
||||
# Simul
|
||||
GET /simul controllers.Simul.home
|
||||
GET /simul/new controllers.Simul.form
|
||||
|
|
|
@ -29,7 +29,7 @@ object Form {
|
|||
of[Double].verifying(hasKey(choices, _))
|
||||
|
||||
def stringIn(choices: Iterable[(String, String)]) =
|
||||
nonEmptyText.verifying(hasKey(choices, _))
|
||||
text.verifying(hasKey(choices, _))
|
||||
|
||||
def hasKey[A](choices: Iterable[(A, _)], key: A) =
|
||||
choices.map(_._1).toList contains key
|
||||
|
|
|
@ -32,6 +32,7 @@ object Permission {
|
|||
case object UserSearch extends Permission("ROLE_USER_SEARCH")
|
||||
case object CloseTeam extends Permission("ROLE_CLOSE_TEAM")
|
||||
case object TerminateTournament extends Permission("ROLE_TERMINATE_TOURNAMENT")
|
||||
case object ManageTournament extends Permission("ROLE_MANAGE_TOURNAMENT")
|
||||
|
||||
case object Hunter extends Permission("ROLE_HUNTER", List(
|
||||
ViewBlurs, MarkEngine, MarkBooster, StaffForum,
|
||||
|
@ -41,14 +42,14 @@ object Permission {
|
|||
case object Admin extends Permission("ROLE_ADMIN", List(
|
||||
Hunter, ModerateForum, IpBan, CloseAccount, ReopenAccount,
|
||||
MarkTroll, SetTitle, SetEmail, ModerateQa, StreamConfig,
|
||||
MessageAnyone, CloseTeam, TerminateTournament))
|
||||
MessageAnyone, CloseTeam, TerminateTournament, ManageTournament))
|
||||
|
||||
case object SuperAdmin extends Permission("ROLE_SUPER_ADMIN", List(Admin))
|
||||
|
||||
private lazy val all: List[Permission] = List(
|
||||
SuperAdmin, Admin, Hunter, ViewBlurs, StaffForum, ModerateForum,
|
||||
UserSpy, MarkTroll, MarkEngine, MarkBooster, IpBan, ModerateQa, StreamConfig,
|
||||
Beta, MessageAnyone, UserSearch, CloseTeam, TerminateTournament)
|
||||
Beta, MessageAnyone, UserSearch, CloseTeam, TerminateTournament, ManageTournament)
|
||||
|
||||
private lazy val allByName: Map[String, Permission] = all map { p => (p.name, p) } toMap
|
||||
|
||||
|
|
|
@ -12,6 +12,33 @@ final class DataForm {
|
|||
|
||||
import DataForm._
|
||||
|
||||
lazy val create = Form(mapping(
|
||||
"clockTime" -> numberInDouble(clockTimePrivateChoices),
|
||||
"clockIncrement" -> numberIn(clockIncrementPrivateChoices),
|
||||
"minutes" -> numberIn(minutePrivateChoices),
|
||||
"waitMinutes" -> numberIn(waitMinuteChoices),
|
||||
"variant" -> number.verifying(validVariantIds contains _),
|
||||
"position" -> nonEmptyText.verifying(positions contains _),
|
||||
"mode" -> optional(number.verifying(Mode.all map (_.id) contains _)),
|
||||
"private" -> optional(text.verifying("on" == _))
|
||||
)(TournamentSetup.apply)(TournamentSetup.unapply)
|
||||
.verifying("Invalid clock", _.validClock)
|
||||
.verifying("Increase tournament duration, or decrease game clock", _.validTiming)
|
||||
) fill TournamentSetup(
|
||||
clockTime = clockTimeDefault,
|
||||
clockIncrement = clockIncrementDefault,
|
||||
minutes = minuteDefault,
|
||||
waitMinutes = waitMinuteDefault,
|
||||
variant = chess.variant.Standard.id,
|
||||
position = StartingPosition.initial.eco,
|
||||
`private` = None,
|
||||
mode = Mode.Rated.id.some)
|
||||
}
|
||||
|
||||
object DataForm {
|
||||
|
||||
import chess.variant._
|
||||
|
||||
val clockTimes: Seq[Double] = Seq(0d, 1 / 2d, 3 / 4d, 1d, 3 / 2d) ++ (2d to 7d by 1d)
|
||||
val clockTimesPrivate: Seq[Double] = clockTimes ++ (10d to 30d by 5d) ++ (40d to 60d by 10d)
|
||||
val clockTimeDefault = 2d
|
||||
|
@ -44,33 +71,6 @@ final class DataForm {
|
|||
}
|
||||
val positionDefault = StartingPosition.initial.eco
|
||||
|
||||
lazy val create = Form(mapping(
|
||||
"clockTime" -> numberInDouble(clockTimePrivateChoices),
|
||||
"clockIncrement" -> numberIn(clockIncrementPrivateChoices),
|
||||
"minutes" -> numberIn(minutePrivateChoices),
|
||||
"waitMinutes" -> numberIn(waitMinuteChoices),
|
||||
"variant" -> number.verifying(validVariantIds contains _),
|
||||
"position" -> nonEmptyText.verifying(positions contains _),
|
||||
"mode" -> optional(number.verifying(Mode.all map (_.id) contains _)),
|
||||
"private" -> optional(text.verifying("on" == _))
|
||||
)(TournamentSetup.apply)(TournamentSetup.unapply)
|
||||
.verifying("Invalid clock", _.validClock)
|
||||
.verifying("Increase tournament duration, or decrease game clock", _.validTiming)
|
||||
) fill TournamentSetup(
|
||||
clockTime = clockTimeDefault,
|
||||
clockIncrement = clockIncrementDefault,
|
||||
minutes = minuteDefault,
|
||||
waitMinutes = waitMinuteDefault,
|
||||
variant = chess.variant.Standard.id,
|
||||
position = StartingPosition.initial.eco,
|
||||
`private` = None,
|
||||
mode = Mode.Rated.id.some)
|
||||
}
|
||||
|
||||
object DataForm {
|
||||
|
||||
import chess.variant._
|
||||
|
||||
val validVariants = List(Standard, Chess960, KingOfTheHill, ThreeCheck, Antichess, Atomic, Horde, RacingKings, Crazyhouse)
|
||||
|
||||
val validVariantIds = validVariants.map(_.id).toSet
|
||||
|
|
|
@ -69,6 +69,8 @@ final class Env(
|
|||
roundMap = roundMap,
|
||||
roundSocketHub = roundSocketHub)
|
||||
|
||||
lazy val crudApi = new crud.CrudApi
|
||||
|
||||
val tourAndRanks = api tourAndRanks _
|
||||
|
||||
private lazy val performance = new Performance
|
||||
|
|
|
@ -72,6 +72,13 @@ object Schedule {
|
|||
case (Bullet, HyperBullet) => true
|
||||
case _ => false
|
||||
}
|
||||
def fromClock(clock: TournamentClock) = {
|
||||
val time = clock.chessClock.estimateTotalTime
|
||||
if (time < 60) HyperBullet
|
||||
else if (time < 180) Bullet
|
||||
else if (time < 480) Blitz
|
||||
else Classical
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait Season
|
||||
|
|
|
@ -101,7 +101,7 @@ object Tournament {
|
|||
val minPlayers = 2
|
||||
|
||||
def make(
|
||||
createdBy: User,
|
||||
createdByUserId: String,
|
||||
clock: TournamentClock,
|
||||
minutes: Int,
|
||||
system: System,
|
||||
|
@ -116,7 +116,7 @@ object Tournament {
|
|||
system = system,
|
||||
clock = clock,
|
||||
minutes = minutes,
|
||||
createdBy = createdBy.id,
|
||||
createdBy = createdByUserId,
|
||||
createdAt = DateTime.now,
|
||||
nbPlayers = 0,
|
||||
variant = variant,
|
||||
|
|
|
@ -42,7 +42,7 @@ private[tournament] final class TournamentApi(
|
|||
def createTournament(setup: TournamentSetup, me: User): Fu[Tournament] = {
|
||||
var variant = chess.variant.Variant orDefault setup.variant
|
||||
val tour = Tournament.make(
|
||||
createdBy = me,
|
||||
createdByUserId = me.id,
|
||||
clock = TournamentClock((setup.clockTime * 60).toInt, setup.clockIncrement),
|
||||
minutes = setup.minutes,
|
||||
waitMinutes = setup.waitMinutes,
|
||||
|
|
|
@ -30,6 +30,7 @@ object TournamentRepo {
|
|||
if (variant.standard) BSONDocument("variant" -> BSONDocument("$exists" -> false))
|
||||
else BSONDocument("variant" -> variant.id)
|
||||
private val nonEmptySelect = BSONDocument("nbPlayers" -> BSONDocument("$ne" -> 0))
|
||||
private val selectUnique = BSONDocument("schedule.freq" -> "unique")
|
||||
|
||||
def byId(id: String): Fu[Option[Tournament]] = coll.find(selectId(id)).one[Tournament]
|
||||
|
||||
|
@ -37,6 +38,9 @@ object TournamentRepo {
|
|||
coll.find(BSONDocument("_id" -> BSONDocument("$in" -> ids)))
|
||||
.cursor[Tournament]().collect[List]()
|
||||
|
||||
def uniqueById(id: String): Fu[Option[Tournament]] =
|
||||
coll.find(selectId(id) ++ selectUnique).one[Tournament]
|
||||
|
||||
def recentAndNext: Fu[List[Tournament]] =
|
||||
coll.find(sinceSelect(DateTime.now minusDays 1))
|
||||
.cursor[Tournament]().collect[List]()
|
||||
|
@ -170,6 +174,12 @@ object TournamentRepo {
|
|||
}.reverse
|
||||
}
|
||||
|
||||
def uniques(max: Int): Fu[List[Tournament]] =
|
||||
coll.find(selectUnique)
|
||||
.sort(BSONDocument("startsAt" -> -1))
|
||||
.hint(BSONDocument("startsAt" -> -1))
|
||||
.cursor[Tournament]().collect[List]()
|
||||
|
||||
def scheduledUnfinished: Fu[List[Tournament]] =
|
||||
coll.find(scheduledSelect ++ unfinishedSelect)
|
||||
.sort(BSONDocument("startsAt" -> 1)).cursor[Tournament]().collect[List]()
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package lila.tournament
|
||||
package crud
|
||||
|
||||
final class CrudApi {
|
||||
|
||||
def list = TournamentRepo uniques 30
|
||||
|
||||
def one(id: String) = TournamentRepo uniqueById id
|
||||
|
||||
def editForm(tour: Tournament) = CrudForm.apply fill CrudForm.Data(
|
||||
name = tour.name,
|
||||
homepageHours = ~tour.spotlight.flatMap(_.homepageHours),
|
||||
clockTime = tour.clock.limitInMinutes,
|
||||
clockIncrement = tour.clock.increment,
|
||||
minutes = tour.minutes,
|
||||
variant = tour.variant.id,
|
||||
mode = tour.mode.id,
|
||||
date = CrudForm.dateFormatter.print(tour.startsAt),
|
||||
dateHour = tour.startsAt.getHourOfDay,
|
||||
dateMinute = tour.startsAt.getMinuteOfHour,
|
||||
image = ~tour.spotlight.flatMap(_.iconImg),
|
||||
headline = tour.spotlight.??(_.headline),
|
||||
description = tour.spotlight.??(_.description))
|
||||
|
||||
def update(old: Tournament, data: CrudForm.Data) =
|
||||
TournamentRepo update updateTour(old, data) void
|
||||
|
||||
def createForm = CrudForm.apply
|
||||
|
||||
def create(data: CrudForm.Data) =
|
||||
TournamentRepo insert updateTour(empty, data) void
|
||||
|
||||
private val empty = Tournament.make(
|
||||
createdByUserId = "lichess",
|
||||
clock = TournamentClock(0, 0),
|
||||
minutes = 0,
|
||||
system = System.Arena,
|
||||
variant = chess.variant.Standard,
|
||||
position = chess.StartingPosition.initial,
|
||||
mode = chess.Mode.Rated,
|
||||
`private` = false,
|
||||
waitMinutes = 0)
|
||||
|
||||
private def updateTour(tour: Tournament, data: CrudForm.Data) = {
|
||||
import data._
|
||||
val clock = TournamentClock((clockTime * 60).toInt, clockIncrement)
|
||||
val v = chess.variant.Variant.orDefault(variant)
|
||||
tour.copy(
|
||||
name = name,
|
||||
clock = clock,
|
||||
minutes = minutes,
|
||||
variant = v,
|
||||
mode = chess.Mode.orDefault(mode),
|
||||
startsAt = actualDate,
|
||||
schedule = Schedule(
|
||||
freq = Schedule.Freq.Unique,
|
||||
speed = Schedule.Speed.fromClock(clock),
|
||||
variant = v,
|
||||
position = chess.StartingPosition.initial,
|
||||
at = actualDate).some,
|
||||
spotlight = Spotlight(
|
||||
headline = headline,
|
||||
description = description,
|
||||
homepageHours = homepageHours.some.filterNot(0 ==),
|
||||
iconFont = none,
|
||||
iconImg = image.some).some)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package lila.tournament
|
||||
package crud
|
||||
|
||||
import org.joda.time.format.DateTimeFormat
|
||||
import org.joda.time.{ DateTime, DateTimeZone }
|
||||
import play.api.data._
|
||||
import play.api.data.Forms._
|
||||
import play.api.data.validation.Constraints._
|
||||
import scala.util.Try
|
||||
|
||||
import chess.Mode
|
||||
import chess.StartingPosition
|
||||
import lila.common.Form._
|
||||
|
||||
object CrudForm {
|
||||
|
||||
import DataForm._
|
||||
|
||||
lazy val apply = Form(mapping(
|
||||
"name" -> nonEmptyText(minLength = 3, maxLength = 40),
|
||||
"homepageHours" -> number(min = 0, max = 24),
|
||||
"clockTime" -> numberInDouble(clockTimePrivateChoices),
|
||||
"clockIncrement" -> numberIn(clockIncrementPrivateChoices),
|
||||
"minutes" -> numberIn(minutePrivateChoices),
|
||||
"variant" -> number.verifying(validVariantIds contains _),
|
||||
"mode" -> number.verifying(Mode.all map (_.id) contains _),
|
||||
"date" -> nonEmptyText.verifying(s => parseDateUTC(s).isDefined),
|
||||
"dateHour" -> number(min = 0, max = 23),
|
||||
"dateMinute" -> number(min = 0, max = 59),
|
||||
"image" -> stringIn(imageChoices),
|
||||
"headline" -> nonEmptyText(minLength = 5, maxLength = 30),
|
||||
"description" -> nonEmptyText(minLength = 10, maxLength = 400)
|
||||
)(CrudForm.Data.apply)(CrudForm.Data.unapply)
|
||||
.verifying("Invalid clock", _.validClock)
|
||||
.verifying("Increase tournament duration, or decrease game clock", _.validTiming)
|
||||
) fill CrudForm.Data(
|
||||
name = "",
|
||||
homepageHours = 0,
|
||||
clockTime = clockTimeDefault,
|
||||
clockIncrement = clockIncrementDefault,
|
||||
minutes = minuteDefault,
|
||||
variant = chess.variant.Standard.id,
|
||||
mode = Mode.Rated.id,
|
||||
date = dateFormatter.print(DateTime.now),
|
||||
dateHour = 0,
|
||||
dateMinute = 0,
|
||||
image = "",
|
||||
headline = "",
|
||||
description = "")
|
||||
|
||||
case class Data(
|
||||
name: String,
|
||||
homepageHours: Int,
|
||||
clockTime: Double,
|
||||
clockIncrement: Int,
|
||||
minutes: Int,
|
||||
variant: Int,
|
||||
mode: Int,
|
||||
date: String,
|
||||
dateHour: Int,
|
||||
dateMinute: Int,
|
||||
image: String,
|
||||
headline: String,
|
||||
description: String) {
|
||||
|
||||
def actualDate = parseDateUTC(date).err(s"Invalid date $date")
|
||||
.withHourOfDay(dateHour)
|
||||
.withMinuteOfHour(dateMinute)
|
||||
|
||||
def validClock = (clockTime + clockIncrement) > 0
|
||||
|
||||
def validTiming = (minutes * 60) >= (3 * estimatedGameDuration)
|
||||
|
||||
private def estimatedGameDuration = 60 * clockTime + 30 * clockIncrement
|
||||
}
|
||||
|
||||
val imageChoices = List(
|
||||
"" -> "Lichess",
|
||||
"chesswhiz.logo.png" -> "ChessWhiz",
|
||||
"chessat3.logo.png" -> "Chessat3")
|
||||
val imageDefault = ""
|
||||
|
||||
val dateFormatter = DateTimeFormat forPattern "yyyy-MM-dd" withZone DateTimeZone.UTC
|
||||
|
||||
private def parseDateUTC(str: String): Option[DateTime] =
|
||||
Try(dateFormatter parseDateTime str).toOption
|
||||
}
|
|
@ -0,0 +1,308 @@
|
|||
.material.form * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.material.form *::after,
|
||||
.material.form *::before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.material.form .button-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.material.form fieldset {
|
||||
margin: 0 0 3rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.material.form .form-radio,
|
||||
.material.form .form-group {
|
||||
position: relative;
|
||||
margin-bottom: 3.25rem;
|
||||
}
|
||||
|
||||
.material.form .form-group.half {
|
||||
display: inline-block;
|
||||
width: 47%;
|
||||
}
|
||||
.material.form .form-group.half .form-group.half {
|
||||
width: 46.5%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.material.form .form-group.half:nth-child(odd) {
|
||||
margin-right: 5%;
|
||||
}
|
||||
.material.form .form-inline > .form-group,
|
||||
.material.form .form-inline > .btn {
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.material.form .form-help {
|
||||
margin-top: 0.125rem;
|
||||
margin-left: 0.125rem;
|
||||
opacity: 0.6;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.material.form .checkbox .form-help, .form-radio .form-help, .form-group .form-help {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
.material.form .checkbox .form-help {
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.material.form .form-radio .form-help {
|
||||
padding-top: 0.25rem;
|
||||
margin-top: -1rem;
|
||||
}
|
||||
|
||||
.material.form .form-group input {
|
||||
height: 1.9rem;
|
||||
}
|
||||
.material.form .form-group textarea {
|
||||
resize: none;
|
||||
}
|
||||
.material.form .form-group select {
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
height: 1.6rem;
|
||||
padding: 0.125rem 0.125rem 0.0625rem;
|
||||
background: none;
|
||||
border: none;
|
||||
line-height: 1.6;
|
||||
box-shadow: none;
|
||||
}
|
||||
.material.form .form-group .control-label {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
pointer-events: none;
|
||||
padding-left: 0.125rem;
|
||||
z-index: 1;
|
||||
opacity: 0.6;
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
-webkit-transition: all 0.28s ease;
|
||||
transition: all 0.28s ease;
|
||||
}
|
||||
.material.form .form-group .bar {
|
||||
position: relative;
|
||||
border-bottom: 0.0625rem solid #999;
|
||||
display: block;
|
||||
}
|
||||
.material.form .form-group .bar::before {
|
||||
content: '';
|
||||
height: 0.125rem;
|
||||
width: 0;
|
||||
left: 50%;
|
||||
bottom: -0.0625rem;
|
||||
position: absolute;
|
||||
background: #337ab7;
|
||||
-webkit-transition: left 0.28s ease, width 0.28s ease;
|
||||
transition: left 0.28s ease, width 0.28s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
.material.form .form-group input,
|
||||
.material.form .form-group textarea {
|
||||
display: block;
|
||||
background: none;
|
||||
padding: 0.125rem 0.125rem 0.0625rem;
|
||||
font-size: 1rem;
|
||||
border-width: 0;
|
||||
border-color: transparent;
|
||||
line-height: 1.9;
|
||||
width: 100%;
|
||||
-webkit-transition: all 0.28s ease;
|
||||
transition: all 0.28s ease;
|
||||
box-shadow: none;
|
||||
}
|
||||
.material.form .form-group input[type="file"] {
|
||||
line-height: 1;
|
||||
}
|
||||
.material.form .form-group input[type="file"] ~ .bar {
|
||||
display: none;
|
||||
}
|
||||
.material.form .form-group * {
|
||||
color: #333;
|
||||
}
|
||||
.material.form .form-group select ~ .control-label,
|
||||
.material.form .form-group input:focus ~ .control-label,
|
||||
.material.form .form-group input:valid ~ .control-label,
|
||||
.material.form .form-group input.form-file ~ .control-label,
|
||||
.material.form .form-group input.has-value ~ .control-label,
|
||||
.material.form .form-group textarea:focus ~ .control-label,
|
||||
.material.form .form-group textarea:valid ~ .control-label,
|
||||
.material.form .form-group textarea.form-file ~ .control-label,
|
||||
.material.form .form-group textarea.has-value ~ .control-label {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.6;
|
||||
top: -1rem;
|
||||
left: 0;
|
||||
}
|
||||
.material.form .form-group select:focus,
|
||||
.material.form .form-group input:focus,
|
||||
.material.form .form-group textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
.material.form .form-group select:focus ~ .control-label,
|
||||
.material.form .form-group input:focus ~ .control-label,
|
||||
.material.form .form-group textarea:focus ~ .control-label {
|
||||
color: #337ab7;
|
||||
}
|
||||
.material.form .form-group select:focus ~ .bar::before,
|
||||
.material.form .form-group input:focus ~ .bar::before,
|
||||
.material.form .form-group textarea:focus ~ .bar::before {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.material.form .checkbox label,
|
||||
.material.form .form-radio label {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding-left: 2rem;
|
||||
text-align: left;
|
||||
display: block;
|
||||
}
|
||||
.material.form .checkbox input,
|
||||
.material.form .form-radio input {
|
||||
width: auto;
|
||||
opacity: 0.00000001;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.material.form .radio {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.material.form .radio .helper {
|
||||
position: absolute;
|
||||
top: -0.25rem;
|
||||
left: -0.25rem;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-size: 1rem;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.material.form .radio .helper::before, .radio .helper::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
margin: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
-webkit-transition: -webkit-transform 0.28s ease;
|
||||
transition: -webkit-transform 0.28s ease;
|
||||
transition: transform 0.28s ease;
|
||||
transition: transform 0.28s ease, -webkit-transform 0.28s ease;
|
||||
border-radius: 50%;
|
||||
border: 0.125rem solid currentColor;
|
||||
}
|
||||
.material.form .radio .helper::after {
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0);
|
||||
background-color: #337ab7;
|
||||
border-color: #337ab7;
|
||||
}
|
||||
.material.form .radio label:hover .helper {
|
||||
color: #337ab7;
|
||||
}
|
||||
.material.form .radio input:checked ~ .helper::after {
|
||||
-webkit-transform: scale(0.5);
|
||||
transform: scale(0.5);
|
||||
}
|
||||
.material.form .radio input:checked ~ .helper::before {
|
||||
color: #337ab7;
|
||||
}
|
||||
|
||||
.material.form .checkbox {
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.material.form .checkbox .helper {
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
z-index: 0;
|
||||
border: 0.125rem solid currentColor;
|
||||
border-radius: 0.0625rem;
|
||||
-webkit-transition: border-color 0.28s ease;
|
||||
transition: border-color 0.28s ease;
|
||||
}
|
||||
.material.form .checkbox .helper::before, .checkbox .helper::after {
|
||||
position: absolute;
|
||||
height: 0;
|
||||
width: 0.2rem;
|
||||
background-color: #337ab7;
|
||||
display: block;
|
||||
-webkit-transform-origin: left top;
|
||||
transform-origin: left top;
|
||||
border-radius: 0.25rem;
|
||||
content: '';
|
||||
-webkit-transition: opacity 0.28s ease, height 0s linear 0.28s;
|
||||
transition: opacity 0.28s ease, height 0s linear 0.28s;
|
||||
opacity: 0;
|
||||
}
|
||||
.material.form .checkbox .helper::before {
|
||||
top: 0.65rem;
|
||||
left: 0.38rem;
|
||||
-webkit-transform: rotate(-135deg);
|
||||
transform: rotate(-135deg);
|
||||
box-shadow: 0 0 0 0.0625rem #fff;
|
||||
}
|
||||
.material.form .checkbox .helper::after {
|
||||
top: 0.3rem;
|
||||
left: 0;
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
.material.form .checkbox label:hover .helper {
|
||||
color: #337ab7;
|
||||
}
|
||||
.material.form .checkbox input:checked ~ .helper {
|
||||
color: #337ab7;
|
||||
}
|
||||
.material.form .checkbox input:checked ~ .helper::after, .checkbox input:checked ~ .helper::before {
|
||||
opacity: 1;
|
||||
-webkit-transition: height 0.28s ease;
|
||||
transition: height 0.28s ease;
|
||||
}
|
||||
.material.form .checkbox input:checked ~ .helper::after {
|
||||
height: 0.5rem;
|
||||
}
|
||||
.material.form .checkbox input:checked ~ .helper::before {
|
||||
height: 1.2rem;
|
||||
-webkit-transition-delay: 0.28s;
|
||||
transition-delay: 0.28s;
|
||||
}
|
||||
|
||||
.material.form .radio + .radio,
|
||||
.material.form .checkbox + .checkbox {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.material.form .has-error .legend.legend, .has-error.form-group .control-label.control-label {
|
||||
color: #d9534f;
|
||||
}
|
||||
.material.form .has-error.form-group .form-help,
|
||||
.material.form .has-error.form-group label strong,
|
||||
.material.form .has-error.form-group .helper, .has-error.checkbox .form-help,
|
||||
.material.form .has-error.checkbox .helper, .has-error.radio .form-help,
|
||||
.material.form .has-error.radio .helper, .has-error.form-radio .form-help,
|
||||
.material.form .has-error.form-radio .helper {
|
||||
color: #d9534f;
|
||||
}
|
||||
.material.form .has-error .bar::before {
|
||||
background: #d9534f;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
h1.lichess_title {
|
||||
text-align: center;
|
||||
}
|
||||
table.slist {
|
||||
font-size: 1.2em;
|
||||
width: 100%;
|
||||
}
|
||||
table.slist td {
|
||||
padding: 20px 10px;
|
||||
}
|
||||
.material.form button {
|
||||
font-size: 1.7em;
|
||||
font-weight: normal;
|
||||
padding: 10px 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.material.form textarea {
|
||||
height: 10em;
|
||||
}
|
||||
.tour_crud a {
|
||||
color: #3893E8!important;
|
||||
}
|
Loading…
Reference in New Issue