manually schedule next swiss round

pull/6639/head
Thibault Duplessis 2020-05-15 12:15:14 -06:00
parent 469b914d32
commit 9734298fe3
15 changed files with 157 additions and 29 deletions

View File

@ -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 =>

View File

@ -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"))
)
)
}
}

View File

@ -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

View File

@ -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.

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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 {

View File

@ -0,0 +1,2 @@
@import '../../../common/css/plugin';
@import '../../../common/css/vendor/flatpickr';

View File

@ -0,0 +1,2 @@
@import '../../../common/css/theme/dark';
@import 'flatpickr';

View File

@ -0,0 +1,2 @@
@import '../../../common/css/theme/light';
@import 'flatpickr';

View File

@ -0,0 +1,2 @@
@import '../../../common/css/theme/transp';
@import 'flatpickr';

View File

@ -61,3 +61,13 @@
margin: 10px auto;
}
}
.schedule-next-round {
@extend %flex-center;
padding: 1em 0;
&.required {
background: $c-accent;
}
input {
margin: auto;
}
}

View File

@ -7,6 +7,7 @@ export type Redraw = () => void;
export interface SwissOpts {
data: SwissData;
userId?: string;
schedule?: boolean;
element: HTMLElement;
$side: JQuery;
socketSend: SocketSend;

View File

@ -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', {