broadcast tournament/round WIP

broadcast-tournament
Thibault Duplessis 2021-04-25 12:30:46 +02:00
parent 6773ef6e46
commit dc240c87c0
31 changed files with 370 additions and 269 deletions

View File

@ -20,7 +20,7 @@ final class RelayRound(
Auth { implicit ctx => me => Auth { implicit ctx => me =>
NoLameOrBot { NoLameOrBot {
WithTourAndRounds(tourId) { trs => WithTourAndRounds(tourId) { trs =>
(trs.tour.ownerId == me.id) ?? { trs.tour.ownedBy(me) ?? {
Ok(html.relay.roundForm.create(env.relay.roundForm.create(trs), trs.tour)).fuccess Ok(html.relay.roundForm.create(env.relay.roundForm.create(trs), trs.tour)).fuccess
} }
} }
@ -33,7 +33,7 @@ final class RelayRound(
me => me =>
NoLameOrBot { NoLameOrBot {
WithTourAndRounds(tourId) { trs => WithTourAndRounds(tourId) { trs =>
(trs.tour.ownerId == me.id) ?? { trs.tour.ownedBy(me) ?? {
env.relay.roundForm env.relay.roundForm
.create(trs) .create(trs)
.bindFromRequest()(ctx.body, formBinding) .bindFromRequest()(ctx.body, formBinding)
@ -58,7 +58,11 @@ final class RelayRound(
.bindFromRequest()(req, formBinding) .bindFromRequest()(req, formBinding)
.fold( .fold(
err => BadRequest(apiFormError(err)).fuccess, err => BadRequest(apiFormError(err)).fuccess,
setup => env.relay.api.create(setup, me, tour) map env.relay.jsonView.admin map JsonOk setup =>
env.relay.api
.create(setup, me, tour)
.map(_ withTour tour)
.map(env.relay.jsonView.admin) map JsonOk
) )
} }
} }
@ -68,7 +72,7 @@ final class RelayRound(
def edit(id: String) = def edit(id: String) =
Auth { implicit ctx => me => Auth { implicit ctx => me =>
OptionFuResult(env.relay.api.byIdAndContributor(id, me)) { rt => OptionFuResult(env.relay.api.byIdAndContributor(id, me)) { rt =>
Ok(html.relay.roundForm.edit(rt, env.relay.roundForm.edit(rt.relay))).fuccess Ok(html.relay.roundForm.edit(rt, env.relay.roundForm.edit(rt.round))).fuccess
} }
} }
@ -93,7 +97,7 @@ final class RelayRound(
case Some(res) => case Some(res) =>
res.fold( res.fold(
{ case (_, err) => BadRequest(apiFormError(err)) }, { case (_, err) => BadRequest(apiFormError(err)) },
rt => JsonOk(env.relay.jsonView.admin(rt.relay)) rt => JsonOk(env.relay.jsonView.admin(rt))
) )
} }
) )
@ -104,12 +108,12 @@ final class RelayRound(
env.relay.api.byIdAndContributor(id, me) flatMap { env.relay.api.byIdAndContributor(id, me) flatMap {
_ ?? { rt => _ ?? { rt =>
env.relay.roundForm env.relay.roundForm
.edit(rt.relay) .edit(rt.round)
.bindFromRequest() .bindFromRequest()
.fold( .fold(
err => fuccess(Left(rt -> err)), err => fuccess(Left(rt -> err)),
data => data =>
env.relay.api.update(rt.relay) { data.update(_, me) }.dmap(_ withTour rt.tour) dmap Right.apply env.relay.api.update(rt.round) { data.update(_, me) }.dmap(_ withTour rt.tour) dmap Right.apply
) dmap some ) dmap some
} }
} }
@ -117,7 +121,7 @@ final class RelayRound(
def reset(id: String) = def reset(id: String) =
Auth { implicit ctx => me => Auth { implicit ctx => me =>
OptionFuResult(env.relay.api.byIdAndContributor(id, me)) { rt => OptionFuResult(env.relay.api.byIdAndContributor(id, me)) { rt =>
env.relay.api.reset(rt.relay, me) inject Redirect(rt.path) env.relay.api.reset(rt.round, me) inject Redirect(rt.path)
} }
} }
@ -127,14 +131,14 @@ final class RelayRound(
pageHit pageHit
WithRelay(ts, rs, id) { rt => WithRelay(ts, rs, id) { rt =>
val sc = val sc =
if (rt.relay.sync.ongoing) if (rt.round.sync.ongoing)
env.study.chapterRepo relaysAndTagsByStudyId rt.relay.studyId flatMap { chapters => env.study.chapterRepo relaysAndTagsByStudyId rt.round.studyId flatMap { chapters =>
chapters.find(_.looksAlive) orElse chapters.headOption match { chapters.find(_.looksAlive) orElse chapters.headOption match {
case Some(chapter) => env.study.api.byIdWithChapter(rt.relay.studyId, chapter.id) case Some(chapter) => env.study.api.byIdWithChapter(rt.round.studyId, chapter.id)
case None => env.study.api byIdWithChapter rt.relay.studyId case None => env.study.api byIdWithChapter rt.round.studyId
} }
} }
else env.study.api byIdWithChapter rt.relay.studyId else env.study.api byIdWithChapter rt.round.studyId
sc flatMap { _ ?? { doShow(rt, _) } } sc flatMap { _ ?? { doShow(rt, _) } }
} }
}, },
@ -142,23 +146,23 @@ final class RelayRound(
me => me =>
env.relay.api.byIdAndContributor(id, me) map { env.relay.api.byIdAndContributor(id, me) map {
case None => NotFound(jsonError("No such broadcast")) case None => NotFound(jsonError("No such broadcast"))
case Some(rt) => JsonOk(env.relay.jsonView.admin(rt.relay)) case Some(rt) => JsonOk(env.relay.jsonView.admin(rt))
} }
) )
def chapter(ts: String, rs: String, id: String, chapterId: String) = def chapter(ts: String, rs: String, id: String, chapterId: String) =
Open { implicit ctx => Open { implicit ctx =>
WithRelay(ts, rs, id) { rt => WithRelay(ts, rs, id) { rt =>
env.study.api.byIdWithChapter(rt.relay.studyId, chapterId) flatMap { env.study.api.byIdWithChapter(rt.round.studyId, chapterId) flatMap {
_ ?? { doShow(rt, _) } _ ?? { doShow(rt, _) }
} }
} }
} }
def cloneRelay(id: String) = def cloneRound(id: String) =
Auth { implicit ctx => me => Auth { implicit ctx => me =>
OptionFuResult(env.relay.api.byIdAndContributor(id, me)) { rt => OptionFuResult(env.relay.api.byIdAndContributor(id, me)) { rt =>
env.relay.api.cloneRelay(rt, me) map { newRelay => env.relay.api.cloneRound(rt, me) map { newRelay =>
Redirect(routes.RelayRound.edit(newRelay.id.value)) Redirect(routes.RelayRound.edit(newRelay.id.value))
} }
} }
@ -184,7 +188,7 @@ final class RelayRound(
)(implicit ctx: Context): Fu[Result] = )(implicit ctx: Context): Fu[Result] =
OptionFuResult(env.relay.api byIdWithTour id) { rt => OptionFuResult(env.relay.api byIdWithTour id) { rt =>
if (rt.tour.slug != ts) Redirect(rt.path).fuccess if (rt.tour.slug != ts) Redirect(rt.path).fuccess
if (rt.relay.slug != rs) Redirect(rt.path).fuccess if (rt.round.slug != rs) Redirect(rt.path).fuccess
else f(rt) else f(rt)
} }
@ -206,7 +210,7 @@ final class RelayRound(
studyC.CanViewResult(oldSc.study) { studyC.CanViewResult(oldSc.study) {
for { for {
(sc, studyData) <- studyC.getJsonData(oldSc) (sc, studyData) <- studyC.getJsonData(oldSc)
data = env.relay.jsonView.makeData(rt.relay, studyData, ctx.userId exists sc.study.canContribute) data = env.relay.jsonView.makeData(rt, studyData, ctx.userId exists sc.study.canContribute)
chat <- studyC.chatOf(sc.study) chat <- studyC.chatOf(sc.study)
sVersion <- env.study.version(sc.study.id) sVersion <- env.study.version(sc.study.id)
streams <- studyC.streamsOf(sc.study) streams <- studyC.streamsOf(sc.study)

View File

@ -26,7 +26,8 @@ final class RelayTour(env: Env) extends LilaController(env) {
if (tour.slug != slug) Redirect(routes.RelayTour.show(tour.slug, tour.id.value)).fuccess if (tour.slug != slug) Redirect(routes.RelayTour.show(tour.slug, tour.id.value)).fuccess
else else
env.relay.api.byTour(tour) map { relays => env.relay.api.byTour(tour) map { relays =>
Ok(html.relay.tour.show(tour, relays)) val markup = tour.markup.map { md => scalatags.Text.all.raw(env.relay.markup(md)) }
Ok(html.relay.tour.show(tour, relays, markup))
} }
} }
} }
@ -45,7 +46,7 @@ final class RelayTour(env: Env) extends LilaController(env) {
err => BadRequest(html.relay.tourForm.create(err)).fuccess, err => BadRequest(html.relay.tourForm.create(err)).fuccess,
setup => setup =>
env.relay.api.tourCreate(setup, me) map { tour => env.relay.api.tourCreate(setup, me) map { tour =>
Redirect(routes.RelayTour.show(tour.slug, tour.id.value)) Redirect(routes.RelayTour.show(tour.slug, tour.id.value)).flashSuccess
} }
) )
} }
@ -53,13 +54,24 @@ final class RelayTour(env: Env) extends LilaController(env) {
def edit(id: String) = Auth { implicit ctx => me => def edit(id: String) = Auth { implicit ctx => me =>
WithTour(id) { tour => WithTour(id) { tour =>
??? tour.ownedBy(me) ?? {
Ok(html.relay.tourForm.edit(tour, env.relay.tourForm.edit(tour))).fuccess
}
} }
} }
def update(id: String) = AuthBody { implicit ctx => me => def update(id: String) = AuthBody { implicit ctx => me =>
WithTour(id) { tour => WithTour(id) { tour =>
??? tour.ownedBy(me) ??
env.relay.tourForm
.edit(tour)
.bindFromRequest()(ctx.body, formBinding)
.fold(
err => BadRequest(html.relay.tourForm.edit(tour, err)).fuccess,
setup =>
env.relay.api.tourUpdate(tour, setup, me) inject
Redirect(routes.RelayTour.show(tour.slug, tour.id.value)).flashSuccess
)
} }
} }

View File

@ -297,8 +297,13 @@ final class Study(
def delete(id: String) = def delete(id: String) =
Auth { _ => me => Auth { _ => me =>
env.study.api.byIdAndOwner(id, me) flatMap { env.study.api.byIdAndOwner(id, me) flatMap {
_ ?? env.study.api.delete _ ?? { study =>
} inject Redirect(routes.Study.mine("hot")) env.study.api.delete(study) >> env.relay.api.deleteRound(lila.relay.RelayRound.Id(id)).map {
case None => Redirect(routes.Study.mine("hot"))
case Some(tour) => Redirect(routes.RelayTour.show(tour.slug, tour.id.value))
}
}
}
} }
def clearChat(id: String) = def clearChat(id: String) =

View File

@ -10,11 +10,7 @@ import lila.clas.{ Clas, Student }
object wall { object wall {
def show( def show(c: Clas, html: Frag, students: List[Student.WithUser])(implicit ctx: Context) =
c: Clas,
html: Frag,
students: List[Student.WithUser]
)(implicit ctx: Context) =
teacherDashboard.layout(c, students.filter(_.student.isActive), "wall")( teacherDashboard.layout(c, students.filter(_.student.isActive), "wall")(
div(cls := "clas-wall__actions")( div(cls := "clas-wall__actions")(
a(dataIcon := "m", href := routes.Clas.wallEdit(c.id.value), cls := "button button-clas text")( a(dataIcon := "m", href := routes.Clas.wallEdit(c.id.value), cls := "button button-clas text")(
@ -30,11 +26,7 @@ object wall {
div(cls := "box__pad clas-wall")(html) div(cls := "box__pad clas-wall")(html)
) )
def edit( def edit(c: Clas, students: List[Student.WithUser], form: Form[_])(implicit ctx: Context) =
c: Clas,
students: List[Student.WithUser],
form: Form[_]
)(implicit ctx: Context) =
teacherDashboard.layout(c, students, "wall")( teacherDashboard.layout(c, students, "wall")(
div(cls := "box-pad clas-wall__edit")( div(cls := "box-pad clas-wall__edit")(
p( p(

View File

@ -0,0 +1,15 @@
package views.html.relay
import controllers.routes
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
object bits {
def howToUse(implicit ctx: Context) =
a(dataIcon := "", cls := "text", href := routes.Page.loneBookmark("broadcasts"))(
"How to use Lichess Broadcasts"
)
}

View File

@ -40,9 +40,12 @@ object index {
div( div(
h2(tr.tour.name), h2(tr.tour.name),
div(cls := "relay-widget__info")( div(cls := "relay-widget__info")(
p(tr.round.description), p(tr.tour.description),
if (tr.ongoing) strong(trans.playingRightNow()) p(
else tr.round.startsAt.map(momentFromNow(_)) strong(tr.round.name),
if (tr.ongoing) trans.playingRightNow()
else tr.round.startsAt.map(momentFromNow(_))
)
) )
) )
) )

View File

@ -22,21 +22,32 @@ object roundForm {
def edit(rt: RelayRound.WithTour, form: Form[Data])(implicit ctx: Context) = def edit(rt: RelayRound.WithTour, form: Form[Data])(implicit ctx: Context) =
layout(rt.fullName)( layout(rt.fullName)(
h1("Edit ", rt.fullName), h1("Edit ", a(href := tour.url(rt.tour))(rt.tour.name), " > ", a(href := rt.path)(rt.round.name)),
inner(form, routes.RelayRound.update(rt.relay.id.value), rt.tour), inner(form, routes.RelayRound.update(rt.round.id.value), rt.tour),
hr, div(cls := "relay-round__actions")(
postForm(action := routes.RelayRound.cloneRelay(rt.relay.id.value))( postForm(action := routes.RelayRound.cloneRound(rt.round.id.value))(
submitButton( submitButton(
cls := "button button-empty confirm", cls := "button button-empty confirm"
title := "Create an new identical broadcast, for another round or a similar tournament" )(
)(cloneBroadcast()) strong(cloneRound()),
), em("Create an new identical round for the same tournament. The games will not be included.")
hr, )
postForm(action := routes.RelayRound.reset(rt.relay.id.value))( ),
submitButton( postForm(action := routes.RelayRound.reset(rt.round.id.value))(
cls := "button button-red button-empty confirm", submitButton(
title := "The source will need to be active in order to re-create the chapters!" cls := "button button-red button-empty confirm"
)(resetBroadcast()) )(
strong(resetRound()),
em(
"Delete all games of this round. The source will need to be active in order to re-create them."
)
)
),
postForm(action := routes.Study.delete(rt.round.id.value))(
submitButton(
cls := "button button-red button-empty confirm"
)(strong(deleteRound()), em("Definitively delete the round and its games."))
)
) )
) )
@ -49,27 +60,11 @@ object roundForm {
main(cls := "page-small box box-pad")(body) main(cls := "page-small box box-pad")(body)
) )
private def inner(form: Form[Data], url: play.api.mvc.Call, tour: RelayTour)(implicit ctx: Context) = private def inner(form: Form[Data], url: play.api.mvc.Call, t: RelayTour)(implicit ctx: Context) =
postForm(cls := "form3", action := url)( postForm(cls := "form3", action := url)(
div(cls := "form-group")( div(cls := "form-group")(bits.howToUse),
a(dataIcon := "", cls := "text", href := routes.Page.loneBookmark("broadcasts"))(
"How to use Lichess Broadcasts"
)
),
form3.globalError(form), form3.globalError(form),
form3.group(form("name"), eventName())(form3.input(_)(autofocus)), form3.group(form("name"), roundName())(form3.input(_)(autofocus)),
form3.group(form("description"), eventDescription())(form3.textarea(_)(rows := 2)),
form3.group(
form("markup"),
fullDescription(),
help = fullDescriptionHelp(
a(
href := "https://guides.github.com/features/mastering-markdown/",
targetBlank
)("Markdown"),
20000.localize
).some
)(form3.textarea(_)(rows := 10)),
form3.group( form3.group(
form("syncUrl"), form("syncUrl"),
sourceUrlOrGameIds(), sourceUrlOrGameIds(),
@ -99,9 +94,8 @@ object roundForm {
half = true half = true
)(form3.input(_, typ = "number")) )(form3.input(_, typ = "number"))
), ),
isGranted(_.Relay) option form3.group(form("credit"), credits())(form3.input(_)),
form3.actions( form3.actions(
a(href := views.html.relay.tour.url(tour))(trans.cancel()), a(href := tour.url(t))(trans.cancel()),
form3.submit(trans.apply()) form3.submit(trans.apply())
) )
) )

View File

@ -60,7 +60,7 @@ object show {
.OpenGraph( .OpenGraph(
title = rt.fullName, title = rt.fullName,
url = s"$netBaseUrl${rt.path}", url = s"$netBaseUrl${rt.path}",
description = shorten(rt.relay.description, 152) description = shorten(rt.tour.description, 152)
) )
.some .some
)( )(
@ -69,13 +69,4 @@ object show {
views.html.study.bits.streamers(streams) views.html.study.bits.streamers(streams)
) )
) )
def widget(rt: lila.relay.RelayRound.WithTour, extraCls: String = "") =
div(cls := s"relay-widget $extraCls", dataIcon := "")(
a(cls := "overlay", href := rt.path),
div(
h3(rt.relay.name),
p(rt.relay.description)
)
)
} }

View File

@ -12,7 +12,7 @@ import lila.relay.{ RelayRound, RelayTour }
object tour { object tour {
def show(t: RelayTour, relays: List[RelayRound.WithTour])(implicit ctx: Context) = def show(t: RelayTour, rounds: List[RelayRound.WithTour], markup: Option[Frag])(implicit ctx: Context) =
views.html.base.layout( views.html.base.layout(
title = t.name, title = t.name,
moreCss = cssTag("relay.index") moreCss = cssTag("relay.index")
@ -20,14 +20,40 @@ object tour {
main(cls := "relay-tour page-small box")( main(cls := "relay-tour page-small box")(
div(cls := "box__top")( div(cls := "box__top")(
h1(t.name), h1(t.name),
a( div(cls := "box__top__actions")(
href := routes.RelayRound.create(t.id.value), a(href := routes.RelayTour.edit(t.id.value), cls := "button button-empty")(trans.edit()),
cls := "new button text", a(
dataIcon := "O" href := routes.RelayRound.create(t.id.value),
)(trans.broadcast.addRound()) cls := "button text",
dataIcon := "O"
)(trans.broadcast.addRound())
)
), ),
div(cls := "list")( standardFlash(cls := "box__pad"),
relays.map { views.html.relay.show.widget(_) } div(cls := "relay-tour__description")(markup getOrElse frag(t.description)),
div(
if (rounds.isEmpty) div(cls := "relay-tour__no-round")("No round has been scheduled yet.")
else
rounds.map { rt =>
div(
cls := List(
"relay-widget" -> true,
"relay-widget--active" -> (rt.round.startedAt.isDefined && !rt.round.finished)
),
dataIcon := ""
)(
a(cls := "overlay", href := rt.path),
div(
h2(rt.round.name),
div(
p(rt.tour.description),
if (rt.round.finished) trans.finished().some
else if (rt.round.startedAt.isDefined) strong(trans.playingRightNow()).some
else rt.round.startsAt.map(momentFromNow(_))
)
)
)
}
) )
) )
} }

View File

@ -16,13 +16,25 @@ object tourForm {
def create(form: Form[Data])(implicit ctx: Context) = def create(form: Form[Data])(implicit ctx: Context) =
layout(newBroadcast.txt())( layout(newBroadcast.txt())(
h1(newBroadcast()), h1(newBroadcast()),
inner(form, routes.RelayTour.create) postForm(cls := "form3", action := routes.RelayTour.create)(
inner(form),
form3.actions(
a(href := routes.RelayTour.index(1))(trans.cancel()),
form3.submit(trans.apply())
)
)
) )
def edit(tour: RelayTour, form: Form[Data])(implicit ctx: Context) = def edit(t: RelayTour, form: Form[Data])(implicit ctx: Context) =
layout(tour.name)( layout(t.name)(
h1("Edit ", tour.name), h1("Edit ", a(href := tour.url(t))(t.name)),
inner(form, routes.RelayTour.update(tour.id.value)) postForm(cls := "form3", action := routes.RelayTour.update(t.id.value))(
inner(form),
form3.actions(
a(href := tour.url(t))(trans.cancel()),
form3.submit(trans.apply())
)
)
) )
private def layout(title: String)(body: Modifier*)(implicit ctx: Context) = private def layout(title: String)(body: Modifier*)(implicit ctx: Context) =
@ -33,37 +45,28 @@ object tourForm {
main(cls := "page-small box box-pad")(body) main(cls := "page-small box box-pad")(body)
) )
private def inner(form: Form[Data], url: play.api.mvc.Call)(implicit ctx: Context) = private def inner(form: Form[Data])(implicit ctx: Context) = frag(
postForm(cls := "form3", action := url)( div(cls := "form-group")(bits.howToUse),
div(cls := "form-group")( form3.globalError(form),
a(dataIcon := "", cls := "text", href := routes.Page.loneBookmark("broadcasts"))( form3.group(form("name"), tournamentName())(form3.input(_)(autofocus)),
"How to use Lichess Broadcasts" form3.group(form("description"), tournamentDescription())(form3.textarea(_)(rows := 2)),
) form3.group(
), form("markup"),
form3.globalError(form), fullDescription(),
form3.group(form("name"), eventName())(form3.input(_)(autofocus)), help = fullDescriptionHelp(
form3.group(form("description"), eventDescription())(form3.textarea(_)(rows := 2)), a(
form3.group( href := "https://guides.github.com/features/mastering-markdown/",
form("markup"), targetBlank
fullDescription(), )("Markdown"),
help = fullDescriptionHelp( 20000.localize
a( ).some
href := "https://guides.github.com/features/mastering-markdown/", )(form3.textarea(_)(rows := 10)),
targetBlank if (isGranted(_.Relay))
)("Markdown"), form3.checkbox(
20000.localize form("official"),
).some raw("Official Lichess broadcast"),
)(form3.textarea(_)(rows := 10)), help = raw("Feature on /broadcast - for admins only").some
if (isGranted(_.Relay))
form3.checkbox(
form("official"),
raw("Official Lichess broadcast"),
help = raw("Feature on /broadcast - for admins only").some
)
else form3.hidden(form("official")),
form3.actions(
a(href := routes.RelayTour.index(1))(trans.cancel()),
form3.submit(trans.apply())
) )
) else form3.hidden(form("official"))
)
} }

View File

@ -20,6 +20,7 @@ db.relay.find().forEach(relay => {
syncedAt: relay.startedAt, syncedAt: relay.startedAt,
}, },
...(relay.markup ? { markup: relay.markup } : {}), ...(relay.markup ? { markup: relay.markup } : {}),
...(relay.credit ? { credit: relay.credit } : {}),
}); });
db.relay.update( db.relay.update(
{ _id: relay._id }, { _id: relay._id },
@ -28,6 +29,9 @@ db.relay.find().forEach(relay => {
$unset: { $unset: {
ownerId: true, ownerId: true,
official: true, official: true,
description: true,
markup: true,
credit: true,
}, },
} }
); );

View File

@ -179,7 +179,7 @@ GET /broadcast/:ts/:rs/$roundId<\w{8}>/$chapterId<\w{8}> controllers.RelayRoun
GET /broadcast/round/$roundId<\w{8}>/edit controllers.RelayRound.edit(roundId: String) GET /broadcast/round/$roundId<\w{8}>/edit controllers.RelayRound.edit(roundId: String)
POST /broadcast/round/$roundId<\w{8}>/edit controllers.RelayRound.update(roundId: String) POST /broadcast/round/$roundId<\w{8}>/edit controllers.RelayRound.update(roundId: String)
POST /broadcast/round/$roundId<\w{8}>/reset controllers.RelayRound.reset(roundId: String) POST /broadcast/round/$roundId<\w{8}>/reset controllers.RelayRound.reset(roundId: String)
POST /broadcast/round/$roundId<\w{8}>/clone controllers.RelayRound.cloneRelay(roundId: String) POST /broadcast/round/$roundId<\w{8}>/clone controllers.RelayRound.cloneRound(roundId: String)
POST /broadcast/round/$roundId<\w{8}>/push controllers.RelayRound.push(roundId: String) POST /broadcast/round/$roundId<\w{8}>/push controllers.RelayRound.push(roundId: String)
# Learn # Learn

View File

@ -1396,19 +1396,21 @@ val `addRound` = new I18nKey("broadcast:addRound")
val `ongoing` = new I18nKey("broadcast:ongoing") val `ongoing` = new I18nKey("broadcast:ongoing")
val `upcoming` = new I18nKey("broadcast:upcoming") val `upcoming` = new I18nKey("broadcast:upcoming")
val `completed` = new I18nKey("broadcast:completed") val `completed` = new I18nKey("broadcast:completed")
val `eventName` = new I18nKey("broadcast:eventName") val `roundName` = new I18nKey("broadcast:roundName")
val `eventDescription` = new I18nKey("broadcast:eventDescription") val `roundNumber` = new I18nKey("broadcast:roundNumber")
val `tournamentName` = new I18nKey("broadcast:tournamentName")
val `tournamentDescription` = new I18nKey("broadcast:tournamentDescription")
val `fullDescription` = new I18nKey("broadcast:fullDescription") val `fullDescription` = new I18nKey("broadcast:fullDescription")
val `fullDescriptionHelp` = new I18nKey("broadcast:fullDescriptionHelp") val `fullDescriptionHelp` = new I18nKey("broadcast:fullDescriptionHelp")
val `sourceUrlOrGameIds` = new I18nKey("broadcast:sourceUrlOrGameIds") val `sourceUrlOrGameIds` = new I18nKey("broadcast:sourceUrlOrGameIds")
val `sourceUrlHelp` = new I18nKey("broadcast:sourceUrlHelp") val `sourceUrlHelp` = new I18nKey("broadcast:sourceUrlHelp")
val `gameIdsHelp` = new I18nKey("broadcast:gameIdsHelp") val `gameIdsHelp` = new I18nKey("broadcast:gameIdsHelp")
val `roundNumber` = new I18nKey("broadcast:roundNumber")
val `startDate` = new I18nKey("broadcast:startDate") val `startDate` = new I18nKey("broadcast:startDate")
val `startDateHelp` = new I18nKey("broadcast:startDateHelp") val `startDateHelp` = new I18nKey("broadcast:startDateHelp")
val `credits` = new I18nKey("broadcast:credits") val `credits` = new I18nKey("broadcast:credits")
val `cloneBroadcast` = new I18nKey("broadcast:cloneBroadcast") val `cloneRound` = new I18nKey("broadcast:cloneRound")
val `resetBroadcast` = new I18nKey("broadcast:resetBroadcast") val `resetRound` = new I18nKey("broadcast:resetRound")
val `deleteRound` = new I18nKey("broadcast:deleteRound")
} }
object streamer { object streamer {

View File

@ -40,6 +40,8 @@ final class Env(
lazy val push = wire[RelayPush] lazy val push = wire[RelayPush]
lazy val markup = wire[RelayMarkup]
private lazy val sync = wire[RelaySync] private lazy val sync = wire[RelaySync]
private lazy val formatApi = wire[RelayFormatApi] private lazy val formatApi = wire[RelayFormatApi]

View File

@ -1,55 +1,54 @@
package lila.relay package lila.relay
import com.github.blemale.scaffeine.LoadingCache
import play.api.libs.json._ import play.api.libs.json._
import scala.concurrent.duration._ import scala.concurrent.duration._
import lila.common.config.BaseUrl import lila.common.config.BaseUrl
import lila.common.Json.jodaWrites import lila.common.Json.jodaWrites
final class JsonView(baseUrl: BaseUrl) { final class JsonView(baseUrl: BaseUrl, markup: RelayMarkup) {
import JsonView._ import JsonView._
private val markdown = new lila.common.Markdown implicit private val roundWithTourWrites = OWrites[RelayRound.WithTour] { rt =>
private val markdownCache: LoadingCache[String, String] = lila.memo.CacheApi.scaffeineNoScheduler
.expireAfterAccess(10 minutes)
.maximumSize(64)
.build(markdown.apply)
implicit private val relayWrites = OWrites[RelayRound] { r =>
Json Json
.obj( .obj(
"id" -> r.id, "id" -> rt.round.id,
"url" -> s"$baseUrl/broadcast/${r.slug}/${r.id}", "url" -> s"$baseUrl${rt.path}",
"name" -> r.name, "name" -> rt.round.name,
"description" -> r.description "tour" -> Json
.obj(
"id" -> rt.tour.id,
"url" -> s"$baseUrl/${rt.tour.slug}/${rt.tour.id}",
"description" -> rt.tour.description,
"active" -> rt.tour.active
)
.add("credit", rt.tour.credit)
.add("markup" -> rt.tour.markup.map(markup.apply))
) )
.add("credit", r.credit) .add("startsAt" -> rt.round.startsAt)
.add("markup" -> r.markup.map(markdownCache.get)) .add("startedAt" -> rt.round.startedAt)
.add("startsAt" -> r.startsAt) .add("finished" -> rt.round.finished.option(true))
.add("startedAt" -> r.startedAt)
.add("finished" -> r.finished.option(true))
} }
def makeData( def makeData(
relay: RelayRound, rt: RelayRound.WithTour,
studyData: lila.study.JsonView.JsData, studyData: lila.study.JsonView.JsData,
canContribute: Boolean canContribute: Boolean
) = ) =
JsData( JsData(
relay = if (canContribute) admin(relay) else public(relay), relay = if (canContribute) admin(rt) else public(rt),
study = studyData.study, study = studyData.study,
analysis = studyData.analysis analysis = studyData.analysis
) )
def public(r: RelayRound) = relayWrites writes r def public(r: RelayRound.WithTour) = roundWithTourWrites writes r
def admin(r: RelayRound) = def admin(rt: RelayRound.WithTour) =
public(r) public(rt)
.add("markdown" -> r.markup) .add("markdown" -> rt.tour.markup)
.add("throttle" -> r.sync.delay) .add("throttle" -> rt.round.sync.delay)
.add("sync" -> r.sync.some) .add("sync" -> rt.round.sync.some)
} }
object JsonView { object JsonView {
@ -58,7 +57,11 @@ object JsonView {
implicit val syncLogEventWrites = Json.writes[SyncLog.Event] implicit val syncLogEventWrites = Json.writes[SyncLog.Event]
implicit val idWrites: Writes[RelayRound.Id] = Writes[RelayRound.Id] { id => implicit val roundIdWrites: Writes[RelayRound.Id] = Writes[RelayRound.Id] { id =>
JsString(id.value)
}
implicit val tourIdWrites: Writes[RelayTour.Id] = Writes[RelayTour.Id] { id =>
JsString(id.value) JsString(id.value)
} }

View File

@ -134,6 +134,9 @@ final class RelayApi(
tourRepo.coll.insert.one(tour) inject tour tourRepo.coll.insert.one(tour) inject tour
} }
def tourUpdate(tour: RelayTour, data: RelayTourForm.Data, user: User): Funit =
tourRepo.coll.update.one($id(tour.id), data.update(tour, user)).void
def create(data: RelayRoundForm.Data, user: User, tour: RelayTour): Fu[RelayRound] = { def create(data: RelayRoundForm.Data, user: User, tour: RelayTour): Fu[RelayRound] = {
val relay = data.make(user, tour) val relay = data.make(user, tour)
roundRepo.coll.insert.one(relay) >> roundRepo.coll.insert.one(relay) >>
@ -163,19 +166,21 @@ final class RelayApi(
} }
def update(from: RelayRound)(f: RelayRound => RelayRound): Fu[RelayRound] = { def update(from: RelayRound)(f: RelayRound => RelayRound): Fu[RelayRound] = {
val relay = f(from) pipe { r => val round = f(from) pipe { r =>
if (r.sync.upstream != from.sync.upstream) r.withSync(_.clearLog) else r if (r.sync.upstream != from.sync.upstream) r.withSync(_.clearLog) else r
} }
studyApi.rename(relay.studyId, Study.Name(relay.name)) >> { studyApi.rename(round.studyId, Study.Name(round.name)) >> {
if (relay == from) fuccess(relay) if (round == from) fuccess(round)
else else
roundRepo.coll.update.one($id(relay.id), relay).void >> { roundRepo.coll.update.one($id(round.id), round).void >> {
(relay.sync.playing != from.sync.playing) ?? publishRelay(relay) (round.sync.playing != from.sync.playing) ?? tourById(round.tourId).flatMap {
} >>- { _.map(round.withTour).map(jsonView.admin) ?? { sendToContributors(round.id, "relayData", _) }
relay.sync.log.events.lastOption.ifTrue(relay.sync.log != from.sync.log).foreach { event =>
sendToContributors(relay.id, "relayLog", JsonView.syncLogEventWrites writes event)
} }
} inject relay } >>- {
round.sync.log.events.lastOption.ifTrue(round.sync.log != from.sync.log).foreach { event =>
sendToContributors(round.id, "relayLog", JsonView.syncLogEventWrites writes event)
}
} inject round
} }
} }
@ -183,13 +188,20 @@ final class RelayApi(
studyApi.deleteAllChapters(relay.studyId, by) >> studyApi.deleteAllChapters(relay.studyId, by) >>
requestPlay(relay.id, v = true) requestPlay(relay.id, v = true)
def cloneRelay(rt: RelayRound.WithTour, by: User): Fu[RelayRound] = def cloneRound(rt: RelayRound.WithTour, by: User): Fu[RelayRound] =
create( create(
RelayRoundForm.Data make rt.relay.copy(name = s"${rt.relay.name} (clone)"), RelayRoundForm.Data make rt.round.copy(name = s"${rt.round.name} (clone)"),
by, by,
rt.tour rt.tour
) )
def deleteRound(roundId: RelayRound.Id): Fu[Option[RelayTour]] =
byIdWithTour(roundId) flatMap {
_ ?? { rt =>
roundRepo.coll.delete.one($id(rt.round.id)) inject rt.tour.some
}
}
def getOngoing(id: RelayRound.Id): Fu[Option[RelayRound.WithTour]] = def getOngoing(id: RelayRound.Id): Fu[Option[RelayRound.WithTour]] =
roundRepo.coll.one[RelayRound]($doc("_id" -> id, "finished" -> false)) flatMap { roundRepo.coll.one[RelayRound]($doc("_id" -> id, "finished" -> false)) flatMap {
_ ?? { relay => _ ?? { relay =>
@ -243,14 +255,11 @@ final class RelayApi(
private[relay] def onStudyRemove(studyId: String) = private[relay] def onStudyRemove(studyId: String) =
roundRepo.coll.delete.one($id(RelayRound.Id(studyId))).void roundRepo.coll.delete.one($id(RelayRound.Id(studyId))).void
private[relay] def publishRelay(relay: RelayRound): Funit =
sendToContributors(relay.id, "relayData", jsonView admin relay)
private def sendToContributors(id: RelayRound.Id, t: String, msg: JsObject): Funit = private def sendToContributors(id: RelayRound.Id, t: String, msg: JsObject): Funit =
studyApi members Study.Id(id.value) map { studyApi members Study.Id(id.value) map {
_.map(_.contributorIds).withFilter(_.nonEmpty) foreach { userIds => _.map(_.contributorIds).withFilter(_.nonEmpty) foreach { userIds =>
import lila.hub.actorApi.socket.SendTos import lila.hub.actorApi.socket.SendTos
import JsonView.idWrites import JsonView.roundIdWrites
import lila.socket.Socket.makeMessage import lila.socket.Socket.makeMessage
val payload = makeMessage(t, msg ++ Json.obj("id" -> id)) val payload = makeMessage(t, msg ++ Json.obj("id" -> id))
lila.common.Bus.publish(SendTos(userIds, payload), "socketUsers") lila.common.Bus.publish(SendTos(userIds, payload), "socketUsers")

View File

@ -55,44 +55,44 @@ final private class RelayFetch(
lila.mon.relay.ongoing(official).update(relays.count(_.tour.official == official)) lila.mon.relay.ongoing(official).update(relays.count(_.tour.official == official))
} }
relays.map { rt => relays.map { rt =>
if (rt.relay.sync.ongoing) processRelay(rt) flatMap { newRelay => if (rt.round.sync.ongoing) processRelay(rt) flatMap { newRelay =>
api.update(rt.relay)(_ => newRelay) api.update(rt.round)(_ => newRelay)
} }
else if (rt.relay.hasStarted) { else if (rt.round.hasStarted) {
logger.info(s"Finish by lack of activity ${rt.relay}") logger.info(s"Finish by lack of activity ${rt.round}")
api.update(rt.relay)(_.finish) api.update(rt.round)(_.finish)
} else if (rt.relay.shouldGiveUp) { } else if (rt.round.shouldGiveUp) {
logger.info(s"Finish for lack of start ${rt.relay}") logger.info(s"Finish for lack of start ${rt.round}")
api.update(rt.relay)(_.finish) api.update(rt.round)(_.finish)
} else fuccess(rt.relay) } else fuccess(rt.round)
}.sequenceFu addEffectAnyway scheduleNext() }.sequenceFu addEffectAnyway scheduleNext()
}.unit }.unit
} }
// no writing the relay; only reading! // no writing the relay; only reading!
def processRelay(rt: RelayRound.WithTour): Fu[RelayRound] = def processRelay(rt: RelayRound.WithTour): Fu[RelayRound] =
if (!rt.relay.sync.playing) fuccess(rt.relay.withSync(_.play)) if (!rt.round.sync.playing) fuccess(rt.round.withSync(_.play))
else else
fetchGames(rt) fetchGames(rt)
.mon(_.relay.fetchTime(rt.tour.official, rt.relay.slug)) .mon(_.relay.fetchTime(rt.tour.official, rt.round.slug))
.addEffect(gs => lila.mon.relay.games(rt.tour.official, rt.relay.slug).update(gs.size).unit) .addEffect(gs => lila.mon.relay.games(rt.tour.official, rt.round.slug).update(gs.size).unit)
.flatMap { games => .flatMap { games =>
sync(rt, games) sync(rt, games)
.withTimeout(7 seconds, SyncResult.Timeout) .withTimeout(7 seconds, SyncResult.Timeout)
.mon(_.relay.syncTime(rt.tour.official, rt.relay.slug)) .mon(_.relay.syncTime(rt.tour.official, rt.round.slug))
.map { res => .map { res =>
res -> rt.relay.withSync(_ addLog SyncLog.event(res.moves, none)) res -> rt.round.withSync(_ addLog SyncLog.event(res.moves, none))
} }
} }
.recover { case e: Exception => .recover { case e: Exception =>
(e match { (e match {
case SyncResult.Timeout => case SyncResult.Timeout =>
if (rt.tour.official) logger.info(s"Sync timeout ${rt.relay}") if (rt.tour.official) logger.info(s"Sync timeout ${rt.round}")
SyncResult.Timeout SyncResult.Timeout
case _ => case _ =>
if (rt.tour.official) logger.info(s"Sync error ${rt.relay} ${e.getMessage take 80}") if (rt.tour.official) logger.info(s"Sync error ${rt.round} ${e.getMessage take 80}")
SyncResult.Error(e.getMessage) SyncResult.Error(e.getMessage)
}) -> rt.relay.withSync(_ addLog SyncLog.event(0, e.some)) }) -> rt.round.withSync(_ addLog SyncLog.event(0, e.some))
} }
.map { case (result, newRelay) => .map { case (result, newRelay) =>
afterSync(result, newRelay withTour rt.tour) afterSync(result, newRelay withTour rt.tour)
@ -102,30 +102,30 @@ final private class RelayFetch(
result match { result match {
case SyncResult.Ok(0, _) => continueRelay(rt) case SyncResult.Ok(0, _) => continueRelay(rt)
case SyncResult.Ok(nbMoves, _) => case SyncResult.Ok(nbMoves, _) =>
lila.mon.relay.moves(rt.tour.official, rt.relay.slug).increment(nbMoves) lila.mon.relay.moves(rt.tour.official, rt.round.slug).increment(nbMoves)
continueRelay(rt.relay.ensureStarted.resume withTour rt.tour) continueRelay(rt.round.ensureStarted.resume withTour rt.tour)
case _ => continueRelay(rt) case _ => continueRelay(rt)
} }
def continueRelay(rt: RelayRound.WithTour): RelayRound = def continueRelay(rt: RelayRound.WithTour): RelayRound =
rt.relay.sync.upstream.fold(rt.relay) { upstream => rt.round.sync.upstream.fold(rt.round) { upstream =>
val seconds = val seconds =
if (rt.relay.sync.log.alwaysFails && !upstream.local) { if (rt.round.sync.log.alwaysFails && !upstream.local) {
rt.relay.sync.log.events.lastOption rt.round.sync.log.events.lastOption
.filterNot(_.isTimeout) .filterNot(_.isTimeout)
.flatMap(_.error) .flatMap(_.error)
.ifTrue(rt.tour.official && rt.relay.hasStarted) foreach { error => .ifTrue(rt.tour.official && rt.round.hasStarted) foreach { error =>
slackApi.broadcastError(rt.relay.id.value, rt.relay.name, error) slackApi.broadcastError(rt.round.id.value, rt.round.name, error)
} }
60 60
} else } else
rt.relay.sync.delay getOrElse { rt.round.sync.delay getOrElse {
if (upstream.local) 3 else 6 if (upstream.local) 3 else 6
} }
rt.relay.withSync { rt.round.withSync {
_.copy( _.copy(
nextAt = DateTime.now plusSeconds { nextAt = DateTime.now plusSeconds {
seconds atLeast { if (rt.relay.sync.log.justTimedOut) 10 else 2 } seconds atLeast { if (rt.round.sync.log.justTimedOut) 10 else 2 }
} some } some
) )
} }
@ -146,7 +146,7 @@ final private class RelayFetch(
) )
private def fetchGames(rt: RelayRound.WithTour): Fu[RelayGames] = private def fetchGames(rt: RelayRound.WithTour): Fu[RelayGames] =
rt.relay.sync.upstream ?? { rt.round.sync.upstream ?? {
case UpstreamIds(ids) => case UpstreamIds(ids) =>
gameRepo.gamesFromSecondary(ids) flatMap gameRepo.gamesFromSecondary(ids) flatMap
gameProxy.upgradeIfPresent flatMap gameProxy.upgradeIfPresent flatMap
@ -161,10 +161,10 @@ final private class RelayFetch(
url, url,
(_, v) => (_, v) =>
Option(v) match { Option(v) match {
case Some(GamesSeenBy(games, seenBy)) if !seenBy(rt.relay.id) => case Some(GamesSeenBy(games, seenBy)) if !seenBy(rt.round.id) =>
GamesSeenBy(games, seenBy + rt.relay.id) GamesSeenBy(games, seenBy + rt.round.id)
case _ => case _ =>
GamesSeenBy(doFetchUrl(url, RelayFetch.maxChapters(rt.tour)), Set(rt.relay.id)) GamesSeenBy(doFetchUrl(url, RelayFetch.maxChapters(rt.tour)), Set(rt.round.id))
} }
) )
.games .games

View File

@ -0,0 +1,17 @@
package lila.relay
import com.github.blemale.scaffeine.LoadingCache
import scala.concurrent.duration._
final class RelayMarkup {
private val renderer = new lila.common.Markdown(autoLink = true)
private val cache: LoadingCache[String, String] = lila.memo.CacheApi.scaffeineNoScheduler
.expireAfterAccess(20 minutes)
.maximumSize(256)
.build(renderer.apply)
def apply(text: String): String = cache.get(text)
}

View File

@ -14,12 +14,12 @@ final class RelayPush(sync: RelaySync, api: RelayApi)(implicit
private val throttler = system.actorOf(Props(new EarlyMultiThrottler(logger = logger))) private val throttler = system.actorOf(Props(new EarlyMultiThrottler(logger = logger)))
def apply(rt: RelayRound.WithTour, pgn: String): Fu[Option[String]] = def apply(rt: RelayRound.WithTour, pgn: String): Fu[Option[String]] =
if (rt.relay.sync.hasUpstream) if (rt.round.sync.hasUpstream)
fuccess("The relay has an upstream URL, and cannot be pushed to.".some) fuccess("The relay has an upstream URL, and cannot be pushed to.".some)
else else
fuccess { fuccess {
throttler ! EarlyMultiThrottler.Work( throttler ! EarlyMultiThrottler.Work(
id = rt.relay.id.value, id = rt.round.id.value,
run = () => pushNow(rt, pgn), run = () => pushNow(rt, pgn),
cooldown = if (rt.tour.official) 3.seconds else 7.seconds cooldown = if (rt.tour.official) 3.seconds else 7.seconds
) )
@ -39,6 +39,6 @@ final class RelayPush(sync: RelaySync, api: RelayApi)(implicit
SyncLog.event(0, e.some) SyncLog.event(0, e.some)
} }
.flatMap { event => .flatMap { event =>
api.update(rt.relay)(_.withSync(_ addLog event)).void api.update(rt.round)(_.withSync(_ addLog event)).void
} }
} }

View File

@ -9,9 +9,6 @@ case class RelayRound(
_id: RelayRound.Id, _id: RelayRound.Id,
tourId: RelayTour.Id, tourId: RelayTour.Id,
name: String, name: String,
description: String,
markup: Option[String] = None,
credit: Option[String] = None,
sync: RelayRound.Sync, sync: RelayRound.Sync,
/* When it's planned to start */ /* When it's planned to start */
startsAt: Option[DateTime], startsAt: Option[DateTime],
@ -61,8 +58,6 @@ case class RelayRound(
def withTour(tour: RelayTour) = RelayRound.WithTour(this, tour) def withTour(tour: RelayTour) = RelayRound.WithTour(this, tour)
def showStartAt = startsAt orElse startedAt
override def toString = s"""relay #$id "$name" $sync""" override def toString = s"""relay #$id "$name" $sync"""
} }
@ -135,12 +130,12 @@ object RelayRound {
case class UpstreamIds(ids: List[lila.game.Game.ID]) extends Upstream case class UpstreamIds(ids: List[lila.game.Game.ID]) extends Upstream
} }
case class WithTour(relay: RelayRound, tour: RelayTour) { case class WithTour(round: RelayRound, tour: RelayTour) {
def fullName = s"${tour.name}${relay.name}" def fullName = s"${tour.name}${round.name}"
def withStudy(study: Study) = WithTourAndStudy(relay, tour, study) def withStudy(study: Study) = WithTourAndStudy(round, tour, study)
def path: String = s"/broadcast/${tour.slug}/${relay.slug}/${relay.id}" def path: String = s"/broadcast/${tour.slug}/${round.slug}/${round.id}"
def path(chapterId: Chapter.Id): String = s"$path/$chapterId" def path(chapterId: Chapter.Id): String = s"$path/$chapterId"
} }

View File

@ -19,14 +19,11 @@ final class RelayRoundForm {
val roundMapping = val roundMapping =
mapping( mapping(
"name" -> cleanText(minLength = 3, maxLength = 80), "name" -> cleanText(minLength = 3, maxLength = 80),
"description" -> cleanText(minLength = 3, maxLength = 400),
"markup" -> optional(cleanText(maxLength = 20000)),
"syncUrl" -> optional { "syncUrl" -> optional {
cleanText(minLength = 8, maxLength = 600).verifying("Invalid source", validSource _) cleanText(minLength = 8, maxLength = 600).verifying("Invalid source", validSource _)
}, },
"syncUrlRound" -> optional(number(min = 1, max = 999)), "syncUrlRound" -> optional(number(min = 1, max = 999)),
"credit" -> optional(cleanNonEmptyText),
"startsAt" -> optional(ISODateTimeOrTimestamp.isoDateTimeOrTimestamp), "startsAt" -> optional(ISODateTimeOrTimestamp.isoDateTimeOrTimestamp),
"throttle" -> optional(number(min = 2, max = 60)) "throttle" -> optional(number(min = 2, max = 60))
)(Data.apply)(Data.unapply) )(Data.apply)(Data.unapply)
@ -38,12 +35,7 @@ final class RelayRoundForm {
s"Maximum rounds per tournament: ${RelayTour.maxRelays}", s"Maximum rounds per tournament: ${RelayTour.maxRelays}",
_ => trs.rounds.sizeIs < RelayTour.maxRelays _ => trs.rounds.sizeIs < RelayTour.maxRelays
) )
}.fill( }.fill(Data(name = s"Round ${trs.rounds.size + 1}"))
Data(
name = s"Round ${trs.rounds.size + 1}",
description = ""
)
)
def edit(r: RelayRound) = Form(roundMapping) fill Data.make(r) def edit(r: RelayRound) = Form(roundMapping) fill Data.make(r)
} }
@ -93,11 +85,8 @@ object RelayRoundForm {
case class Data( case class Data(
name: String, name: String,
description: String,
markup: Option[String] = None,
syncUrl: Option[String] = None, syncUrl: Option[String] = None,
syncUrlRound: Option[Int] = None, syncUrlRound: Option[Int] = None,
credit: Option[String] = None,
startsAt: Option[DateTime] = None, startsAt: Option[DateTime] = None,
throttle: Option[Int] = None throttle: Option[Int] = None
) { ) {
@ -116,12 +105,9 @@ object RelayRoundForm {
def update(relay: RelayRound, user: User) = def update(relay: RelayRound, user: User) =
relay.copy( relay.copy(
name = name, name = name,
description = description,
markup = markup,
sync = makeSync(user) pipe { sync => sync = makeSync(user) pipe { sync =>
if (relay.sync.playing) sync.play else sync if (relay.sync.playing) sync.play else sync
}, },
credit = credit,
startsAt = startsAt, startsAt = startsAt,
finished = relay.finished && startsAt.fold(true)(_.isBeforeNow) finished = relay.finished && startsAt.fold(true)(_.isBeforeNow)
) )
@ -144,10 +130,7 @@ object RelayRoundForm {
_id = RelayRound.makeId, _id = RelayRound.makeId,
tourId = tour.id, tourId = tour.id,
name = name, name = name,
description = description,
markup = markup,
sync = makeSync(user), sync = makeSync(user),
credit = credit,
createdAt = DateTime.now, createdAt = DateTime.now,
finished = false, finished = false,
startsAt = startsAt, startsAt = startsAt,
@ -160,14 +143,11 @@ object RelayRoundForm {
def make(relay: RelayRound) = def make(relay: RelayRound) =
Data( Data(
name = relay.name, name = relay.name,
description = relay.description,
markup = relay.markup,
syncUrl = relay.sync.upstream map { syncUrl = relay.sync.upstream map {
case url: RelayRound.Sync.UpstreamUrl => url.withRound.url case url: RelayRound.Sync.UpstreamUrl => url.withRound.url
case RelayRound.Sync.UpstreamIds(ids) => ids mkString " " case RelayRound.Sync.UpstreamIds(ids) => ids mkString " "
}, },
syncUrlRound = relay.sync.upstream.flatMap(_.asUrl).flatMap(_.withRound.round), syncUrlRound = relay.sync.upstream.flatMap(_.asUrl).flatMap(_.withRound.round),
credit = relay.credit,
startsAt = relay.startsAt, startsAt = relay.startsAt,
throttle = relay.sync.delay throttle = relay.sync.delay
) )

View File

@ -14,7 +14,7 @@ final private class RelaySync(
private type NbMoves = Int private type NbMoves = Int
def apply(rt: RelayRound.WithTour, games: RelayGames): Fu[SyncResult.Ok] = def apply(rt: RelayRound.WithTour, games: RelayGames): Fu[SyncResult.Ok] =
studyApi byId rt.relay.studyId orFail "Missing relay study!" flatMap { study => studyApi byId rt.round.studyId orFail "Missing relay study!" flatMap { study =>
chapterRepo orderedByStudy study.id flatMap { chapters => chapterRepo orderedByStudy study.id flatMap { chapters =>
RelayInputSanity(chapters, games) match { RelayInputSanity(chapters, games) match {
case Some(fail) => fufail(fail.msg) case Some(fail) => fufail(fail.msg)

View File

@ -9,6 +9,7 @@ case class RelayTour(
name: String, name: String,
description: String, description: String,
markup: Option[String] = None, markup: Option[String] = None,
credit: Option[String] = None,
ownerId: User.ID, ownerId: User.ID,
createdAt: DateTime, createdAt: DateTime,
official: Boolean, official: Boolean,
@ -23,6 +24,8 @@ case class RelayTour(
} }
def withRounds(rounds: List[RelayRound]) = RelayTour.WithRounds(this, rounds) def withRounds(rounds: List[RelayRound]) = RelayTour.WithRounds(this, rounds)
def ownedBy(user: User) = ownerId == user.id
} }
object RelayTour { object RelayTour {

View File

@ -27,7 +27,7 @@ final class RelayTourForm {
def create = form def create = form
def edit(r: RelayTour) = form fill Data.make(r) def edit(t: RelayTour) = form fill Data.make(t)
} }
object RelayTourForm { object RelayTourForm {

View File

@ -7,17 +7,19 @@
<string name="ongoing">Ongoing</string> <string name="ongoing">Ongoing</string>
<string name="upcoming">Upcoming</string> <string name="upcoming">Upcoming</string>
<string name="completed">Completed</string> <string name="completed">Completed</string>
<string name="eventName">Event name</string> <string name="roundName">Round name</string>
<string name="eventDescription">Short event description</string> <string name="roundNumber">Round number</string>
<string name="fullDescription">Full event description</string> <string name="tournamentName">Tournament name</string>
<string name="fullDescriptionHelp">Optional long description of the broadcast. %1$s is available. Length must be less than %2$s characters.</string> <string name="tournamentDescription">Short tournament description</string>
<string name="fullDescription">Full tournament description</string>
<string name="fullDescriptionHelp">Optional long description of the tournament. %1$s is available. Length must be less than %2$s characters.</string>
<string name="sourceUrlOrGameIds">Source URL, or game IDs</string> <string name="sourceUrlOrGameIds">Source URL, or game IDs</string>
<string name="sourceUrlHelp">URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.</string> <string name="sourceUrlHelp">URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.</string>
<string name="gameIdsHelp">Alternatively, you can enter up to 64 Lichess game IDs, separated by spaces.</string> <string name="gameIdsHelp">Alternatively, you can enter up to 64 Lichess game IDs, separated by spaces.</string>
<string name="roundNumber">Round number</string>
<string name="startDate">Start date in your own timezone</string> <string name="startDate">Start date in your own timezone</string>
<string name="startDateHelp">Optional, if you know when the event starts</string> <string name="startDateHelp">Optional, if you know when the event starts</string>
<string name="credits">Credit the source</string> <string name="credits">Credit the source</string>
<string name="cloneBroadcast">Clone the broadcast</string> <string name="cloneRound">Clone this round</string>
<string name="resetBroadcast">Reset the broadcast</string> <string name="resetRound">Reset this round</string>
<string name="deleteRound">Delete this round</string>
</resources> </resources>

View File

@ -244,7 +244,7 @@ export function view(ctrl: StudyFormCtrl): VNode {
? h( ? h(
'a', 'a',
{ {
attrs: { href: `/broadcast/-/${data.id}/edit` }, attrs: { href: `/broadcast/round/${data.id}/edit` },
}, },
'Broadcast settings' 'Broadcast settings'
) )

View File

@ -2,3 +2,4 @@
@import '../../../common/css/form/form3'; @import '../../../common/css/form/form3';
@import '../../../common/css/form/cmn-toggle'; @import '../../../common/css/form/cmn-toggle';
@import '../../../common/css/vendor/flatpickr'; @import '../../../common/css/vendor/flatpickr';
@import '../relay/form';

View File

@ -1,4 +1,4 @@
@import '../../../common/css/plugin'; @import '../../../common/css/plugin';
@import '../../../common/css/form/form3'; @import '../../../common/css/form/form3';
@import '../../../common/css/form/captcha'; @import '../../../common/css/component/slist';
@import '../relay/index'; @import '../relay/relay';

View File

@ -0,0 +1,21 @@
.relay-round__actions {
margin: 2em 0;
border-top: $border;
padding-top: 3em;
form {
.button {
@extend %flex-column;
text-align: left;
width: 100%;
strong {
display: block;
}
em {
color: $c-font-dim;
text-transform: none;
white-space: normal;
}
}
}
}

View File

@ -1,15 +0,0 @@
@import 'widget';
.relay-index {
.box__top {
flex-flow: row nowrap;
}
.new::before {
font-size: 3em;
}
section {
margin-bottom: 2em;
}
}

View File

@ -0,0 +1,32 @@
@import 'widget';
.relay-index {
.box__top {
flex-flow: row nowrap;
}
.new::before {
font-size: 3em;
}
section {
margin-bottom: 2em;
}
}
.relay-tour {
&__description {
@extend %box-padding, %box-neat;
margin: 2em auto 2em auto;
padding: 1.5em 2.5em;
background: $c-bg-zebra;
width: 70%;
}
&__no-round {
@extend %box-padding;
margin: 3em;
text-align: center;
}
}