manually schedule next swiss round
parent
469b914d32
commit
9734298fe3
|
@ -160,11 +160,25 @@ final class Swiss(
|
|||
.bindFromRequest
|
||||
.fold(
|
||||
err => BadRequest(html.swiss.form.edit(swiss, err)).fuccess,
|
||||
data => env.swiss.api.update(swiss, data) inject Redirect(routes.Swiss.show(id)).flashSuccess
|
||||
data => env.swiss.api.update(swiss, data) inject Redirect(routes.Swiss.show(id))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def scheduleNextRound(id: String) =
|
||||
AuthBody { implicit ctx => me =>
|
||||
WithEditableSwiss(id, me) { swiss =>
|
||||
implicit val req = ctx.body
|
||||
env.swiss.forms
|
||||
.nextRound(swiss)
|
||||
.bindFromRequest
|
||||
.fold(
|
||||
err => Redirect(routes.Swiss.show(id)).fuccess,
|
||||
date => env.swiss.api.scheduleNextRound(swiss, date) inject Redirect(routes.Swiss.show(id))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def terminate(id: String) =
|
||||
Auth { implicit ctx => me =>
|
||||
WithEditableSwiss(id, me) { swiss =>
|
||||
|
|
|
@ -17,29 +17,37 @@ object show {
|
|||
s: Swiss,
|
||||
data: play.api.libs.json.JsObject,
|
||||
chatOption: Option[lila.chat.UserChat.Mine]
|
||||
)(implicit ctx: Context): Frag =
|
||||
)(implicit ctx: Context): Frag = {
|
||||
val isDirector = ctx.userId.has(s.createdBy)
|
||||
val hasScheduleInput = isDirector && s.settings.manualRounds && s.isNotFinished
|
||||
views.html.base.layout(
|
||||
title = s"${s.name} #${s.id}",
|
||||
moreJs = frag(
|
||||
jsAt(s"compiled/lichess.swiss${isProd ?? ".min"}.js"),
|
||||
hasScheduleInput option flatpickrTag,
|
||||
embedJsUnsafe(s"""LichessSwiss.start(${safeJsonValue(
|
||||
Json.obj(
|
||||
"data" -> data,
|
||||
"i18n" -> bits.jsI18n,
|
||||
"userId" -> ctx.userId,
|
||||
"chat" -> chatOption.map { c =>
|
||||
chat.json(
|
||||
c.chat,
|
||||
name = trans.chatRoom.txt(),
|
||||
timeout = c.timeout,
|
||||
public = true,
|
||||
resourceId = lila.chat.Chat.ResourceId(s"swiss/${c.chat.id}")
|
||||
)
|
||||
}
|
||||
)
|
||||
Json
|
||||
.obj(
|
||||
"data" -> data,
|
||||
"i18n" -> bits.jsI18n,
|
||||
"userId" -> ctx.userId,
|
||||
"chat" -> chatOption.map { c =>
|
||||
chat.json(
|
||||
c.chat,
|
||||
name = trans.chatRoom.txt(),
|
||||
timeout = c.timeout,
|
||||
public = true,
|
||||
resourceId = lila.chat.Chat.ResourceId(s"swiss/${c.chat.id}")
|
||||
)
|
||||
}
|
||||
)
|
||||
.add("schedule" -> hasScheduleInput)
|
||||
)})""")
|
||||
),
|
||||
moreCss = cssTag("swiss.show"),
|
||||
moreCss = frag(
|
||||
cssTag("swiss.show"),
|
||||
hasScheduleInput option cssTag("flatpickr")
|
||||
),
|
||||
chessground = false,
|
||||
openGraph = lila.app.ui
|
||||
.OpenGraph(
|
||||
|
@ -61,4 +69,5 @@ object show {
|
|||
div(cls := "swiss__main")(div(cls := "box"))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -258,6 +258,7 @@ POST /swiss/$id<\w{8}>/terminate controllers.Swiss.terminate(id: St
|
|||
GET /swiss/$id<\w{8}>/standing/:page controllers.Swiss.standing(id: String, page: Int)
|
||||
GET /swiss/$id<\w{8}>/page-of/:user controllers.Swiss.pageOf(id: String, user: String)
|
||||
GET /swiss/$id<\w{8}>/player/:user controllers.Swiss.player(id: String, user: String)
|
||||
POST /swiss/$id<\w{8}>/schedule-next-round controllers.Swiss.scheduleNextRound(id: String)
|
||||
|
||||
# Simul
|
||||
GET /simul controllers.Simul.home
|
||||
|
|
|
@ -106,8 +106,10 @@ final class ChatApi(
|
|||
|
||||
def system(chatId: Chat.Id, text: String, busChan: BusChan.Select): Funit = {
|
||||
val line = UserLine(systemUserId, None, text, troll = false, deleted = false)
|
||||
pushLine(chatId, line) >>-
|
||||
pushLine(chatId, line) >>- {
|
||||
cached.invalidate(chatId)
|
||||
publish(chatId, actorApi.ChatLine(chatId, line), busChan)
|
||||
}
|
||||
}
|
||||
|
||||
// like system, but not persisted.
|
||||
|
|
|
@ -84,7 +84,9 @@ object Swiss {
|
|||
description: Option[String] = None,
|
||||
hasChat: Boolean = true,
|
||||
roundInterval: FiniteDuration
|
||||
)
|
||||
) {
|
||||
def manualRounds = roundInterval.toSeconds == 0
|
||||
}
|
||||
|
||||
def makeScore(points: Points, tieBreak: TieBreak, perf: Performance) =
|
||||
Score(
|
||||
|
|
|
@ -7,6 +7,7 @@ import reactivemongo.akkastream.cursorProducer
|
|||
import reactivemongo.api._
|
||||
import reactivemongo.api.bson._
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.chaining._
|
||||
|
||||
import lila.chat.Chat
|
||||
import lila.common.{ Bus, GreatPlayer, LightUser }
|
||||
|
@ -88,7 +89,9 @@ final class SwissApi(
|
|||
clock = data.clock,
|
||||
variant = data.realVariant,
|
||||
startsAt = data.startsAt.ifTrue(old.isCreated) | old.startsAt,
|
||||
nextRoundAt = if (old.isCreated) Some(data.startsAt | old.startsAt) else old.nextRoundAt,
|
||||
nextRoundAt =
|
||||
if (old.isCreated) Some(data.startsAt | old.startsAt)
|
||||
else old.nextRoundAt,
|
||||
settings = old.settings.copy(
|
||||
nbRounds = data.nbRounds,
|
||||
rated = data.rated | old.settings.rated,
|
||||
|
@ -96,9 +99,30 @@ final class SwissApi(
|
|||
hasChat = data.hasChat | old.settings.hasChat,
|
||||
roundInterval = data.roundInterval.fold(old.settings.roundInterval)(_.seconds)
|
||||
)
|
||||
)
|
||||
) pipe { s =>
|
||||
if (
|
||||
s.isStarted && s.nbOngoing == 0 && (s.nextRoundAt.isEmpty || old.settings.manualRounds) && !s.settings.manualRounds
|
||||
)
|
||||
s.copy(nextRoundAt = DateTime.now.plusSeconds(s.settings.roundInterval.toSeconds.toInt).some)
|
||||
else if (s.settings.manualRounds && !old.settings.manualRounds)
|
||||
s.copy(nextRoundAt = none)
|
||||
else s
|
||||
}
|
||||
)
|
||||
.void
|
||||
.void >>- socket.reload(swiss.id)
|
||||
}
|
||||
|
||||
def scheduleNextRound(swiss: Swiss, date: DateTime): Funit =
|
||||
Sequencing(swiss.id)(notFinishedById) { old =>
|
||||
old.settings.manualRounds ?? {
|
||||
if (old.isCreated) colls.swiss.updateField($id(old.id), "startsAt", date).void
|
||||
else if (old.isStarted && old.nbOngoing == 0)
|
||||
colls.swiss.updateField($id(old.id), "nextRoundAt", date).void >>- {
|
||||
val show = org.joda.time.format.DateTimeFormat.forStyle("MS") print date
|
||||
systemChat(swiss.id, s"Round ${swiss.round.value + 1} scheduled at $show UTC")
|
||||
}
|
||||
else funit
|
||||
} >>- socket.reload(swiss.id)
|
||||
}
|
||||
|
||||
def join(id: Swiss.Id, me: User, isInTeam: TeamID => Boolean): Fu[Boolean] =
|
||||
|
@ -256,8 +280,14 @@ final class SwissApi(
|
|||
val winner = game.winnerColor
|
||||
.map(_.fold(pairing.white, pairing.black))
|
||||
.flatMap(playerNumberHandler.writeOpt)
|
||||
colls.pairing.updateField($id(game.id), SwissPairing.Fields.status, winner | BSONNull).void >>
|
||||
colls.swiss.update.one($id(swiss.id), $inc("nbOngoing" -> -1)) >>
|
||||
colls.pairing.updateField($id(game.id), SwissPairing.Fields.status, winner | BSONNull).void >> {
|
||||
if (swiss.nbOngoing > 0)
|
||||
colls.swiss.update.one($id(swiss.id), $inc("nbOngoing" -> -1))
|
||||
else
|
||||
fuccess {
|
||||
logger.warn(s"swiss ${swiss.id} nbOngoing = ${swiss.nbOngoing}")
|
||||
}
|
||||
} >>
|
||||
game.playerWhoDidNotMove.flatMap(_.userId).?? { absent =>
|
||||
SwissPlayer.fields { f =>
|
||||
colls.player
|
||||
|
@ -265,8 +295,11 @@ final class SwissApi(
|
|||
.void
|
||||
}
|
||||
} >> {
|
||||
(swiss.nbOngoing == 1) ?? {
|
||||
(swiss.nbOngoing <= 1) ?? {
|
||||
if (swiss.round.value == swiss.settings.nbRounds) doFinish(swiss)
|
||||
else if (swiss.settings.manualRounds) fuccess {
|
||||
systemChat(swiss.id, s"Round ${swiss.round.value + 1} needs to be scheduled.")
|
||||
}
|
||||
else
|
||||
colls.swiss
|
||||
.updateField(
|
||||
|
|
|
@ -38,7 +38,7 @@ final class SwissForm(implicit mode: Mode) {
|
|||
"nbRounds" -> number(min = 3, max = 100),
|
||||
"description" -> optional(nonEmptyText),
|
||||
"hasChat" -> optional(boolean),
|
||||
"roundInterval" -> optional(number(min = 5, max = 3600))
|
||||
"roundInterval" -> optional(numberIn(roundIntervals))
|
||||
)(SwissData.apply)(SwissData.unapply)
|
||||
)
|
||||
|
||||
|
@ -69,6 +69,13 @@ final class SwissForm(implicit mode: Mode) {
|
|||
hasChat = s.settings.hasChat.some,
|
||||
roundInterval = s.settings.roundInterval.toSeconds.toInt.some
|
||||
)
|
||||
|
||||
def nextRound(s: Swiss) =
|
||||
Form(
|
||||
single(
|
||||
"date" -> inTheFuture(ISODateTimeOrTimestamp.isoDateTimeOrTimestamp)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
object SwissForm {
|
||||
|
@ -83,11 +90,16 @@ object SwissForm {
|
|||
)
|
||||
|
||||
val roundIntervals: Seq[Int] =
|
||||
Seq(5, 10, 20, 30, 45, 60, 90, 120, 180, 300, 600, 900, 1200, 1800, 2700, 3600)
|
||||
Seq(5, 10, 20, 30, 45, 60, 90, 120, 180, 300, 600, 900, 1200, 1800, 2700, 3600, 24 * 3600, 0)
|
||||
|
||||
val roundIntervalChoices = options(
|
||||
roundIntervals,
|
||||
s => if (s < 60) s"$s seconds" else s"${s / 60} minute(s)"
|
||||
s =>
|
||||
if (s == 0) s"Manually schedule each round"
|
||||
else if (s < 60) s"$s seconds"
|
||||
else if (s < 3600) s"${s / 60} minute(s)"
|
||||
else if (s < 24 * 3600) s"${s / 3600} hour(s)"
|
||||
else s"${s / 24 / 3600} days(s)"
|
||||
)
|
||||
|
||||
case class SwissData(
|
||||
|
|
|
@ -341,6 +341,7 @@ interface JQuery {
|
|||
highcharts(conf?: any): any;
|
||||
slider(key: string, value: any): any;
|
||||
slider(opts: any): any;
|
||||
flatpickr(opts: any): any;
|
||||
}
|
||||
|
||||
declare namespace PowerTip {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
@import '../../../common/css/plugin';
|
||||
@import '../../../common/css/vendor/flatpickr';
|
|
@ -0,0 +1,2 @@
|
|||
@import '../../../common/css/theme/dark';
|
||||
@import 'flatpickr';
|
|
@ -0,0 +1,2 @@
|
|||
@import '../../../common/css/theme/light';
|
||||
@import 'flatpickr';
|
|
@ -0,0 +1,2 @@
|
|||
@import '../../../common/css/theme/transp';
|
||||
@import 'flatpickr';
|
|
@ -61,3 +61,13 @@
|
|||
margin: 10px auto;
|
||||
}
|
||||
}
|
||||
.schedule-next-round {
|
||||
@extend %flex-center;
|
||||
padding: 1em 0;
|
||||
&.required {
|
||||
background: $c-accent;
|
||||
}
|
||||
input {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ export type Redraw = () => void;
|
|||
export interface SwissOpts {
|
||||
data: SwissData;
|
||||
userId?: string;
|
||||
schedule?: boolean;
|
||||
element: HTMLElement;
|
||||
$side: JQuery;
|
||||
socketSend: SocketSend;
|
||||
|
|
|
@ -41,6 +41,7 @@ function created(ctrl: SwissCtrl): MaybeVNodes {
|
|||
const pag = pagination.players(ctrl);
|
||||
return [
|
||||
header(ctrl),
|
||||
nextRound(ctrl),
|
||||
controls(ctrl, pag),
|
||||
standing(ctrl, pag, 'created'),
|
||||
h('blockquote.pull-quote', [
|
||||
|
@ -61,6 +62,7 @@ function started(ctrl: SwissCtrl): MaybeVNodes {
|
|||
return [
|
||||
header(ctrl),
|
||||
joinTheGame(ctrl) || notice(ctrl),
|
||||
nextRound(ctrl),
|
||||
controls(ctrl, pag),
|
||||
standing(ctrl, pag, 'started')
|
||||
];
|
||||
|
@ -86,6 +88,39 @@ function controls(ctrl: SwissCtrl, pag): VNode {
|
|||
]);
|
||||
}
|
||||
|
||||
function nextRound(ctrl: SwissCtrl): VNode | undefined {
|
||||
if (!ctrl.opts.schedule || ctrl.data.nbOngoing) return;
|
||||
return h('form.schedule-next-round', {
|
||||
class: {
|
||||
required: !ctrl.data.nextRound
|
||||
},
|
||||
attrs: {
|
||||
action: `/swiss/${ctrl.data.id}/schedule-next-round`,
|
||||
method: 'post'
|
||||
}
|
||||
}, [
|
||||
h('input', {
|
||||
attrs: {
|
||||
name: 'date',
|
||||
placeholder: 'Schedule the next round',
|
||||
value: ctrl.data.nextRound?.at
|
||||
},
|
||||
hook: onInsert((el: HTMLInputElement) =>
|
||||
setTimeout(() => $(el).flatpickr({
|
||||
minDate: 'today',
|
||||
maxDate: new Date(Date.now() + 1000 * 3600 * 24 * 31),
|
||||
dateFormat: 'Z',
|
||||
altInput: true,
|
||||
altFormat: 'Y-m-d h:i K',
|
||||
enableTime: true,
|
||||
onClose() {
|
||||
(el.parentNode as HTMLFormElement).submit();
|
||||
}
|
||||
}), 600))
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
function joinButton(ctrl: SwissCtrl): VNode | undefined {
|
||||
const d = ctrl.data;
|
||||
if (!ctrl.opts.userId) return h('a.fbt.text.highlight', {
|
||||
|
|
Loading…
Reference in New Issue