broadcast tournament/round WIP
parent
6773ef6e46
commit
dc240c87c0
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) =
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
|
@ -40,12 +40,15 @@ 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(
|
||||||
|
strong(tr.round.name),
|
||||||
|
if (tr.ongoing) trans.playingRightNow()
|
||||||
else tr.round.startsAt.map(momentFromNow(_))
|
else tr.round.startsAt.map(momentFromNow(_))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
st.section(cls := "infinite-scroll")(
|
st.section(cls := "infinite-scroll")(
|
||||||
|
|
|
@ -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.round.id.value))(
|
||||||
postForm(action := routes.RelayRound.reset(rt.relay.id.value))(
|
|
||||||
submitButton(
|
submitButton(
|
||||||
cls := "button button-red button-empty confirm",
|
cls := "button button-red button-empty confirm"
|
||||||
title := "The source will need to be active in order to re-create the chapters!"
|
)(
|
||||||
)(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())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
div(cls := "box__top__actions")(
|
||||||
|
a(href := routes.RelayTour.edit(t.id.value), cls := "button button-empty")(trans.edit()),
|
||||||
a(
|
a(
|
||||||
href := routes.RelayRound.create(t.id.value),
|
href := routes.RelayRound.create(t.id.value),
|
||||||
cls := "new button text",
|
cls := "button text",
|
||||||
dataIcon := "O"
|
dataIcon := "O"
|
||||||
)(trans.broadcast.addRound())
|
)(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(_))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,16 +45,11 @@ 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")(
|
|
||||||
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"), tournamentName())(form3.input(_)(autofocus)),
|
||||||
form3.group(form("description"), eventDescription())(form3.textarea(_)(rows := 2)),
|
form3.group(form("description"), tournamentDescription())(form3.textarea(_)(rows := 2)),
|
||||||
form3.group(
|
form3.group(
|
||||||
form("markup"),
|
form("markup"),
|
||||||
fullDescription(),
|
fullDescription(),
|
||||||
|
@ -60,10 +67,6 @@ object tourForm {
|
||||||
raw("Official Lichess broadcast"),
|
raw("Official Lichess broadcast"),
|
||||||
help = raw("Feature on /broadcast - for admins only").some
|
help = raw("Feature on /broadcast - for admins only").some
|
||||||
)
|
)
|
||||||
else form3.hidden(form("official")),
|
else form3.hidden(form("official"))
|
||||||
form3.actions(
|
|
||||||
a(href := routes.RelayTour.index(1))(trans.cancel()),
|
|
||||||
form3.submit(trans.apply())
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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", r.credit)
|
.add("credit", rt.tour.credit)
|
||||||
.add("markup" -> r.markup.map(markdownCache.get))
|
.add("markup" -> rt.tour.markup.map(markup.apply))
|
||||||
.add("startsAt" -> r.startsAt)
|
)
|
||||||
.add("startedAt" -> r.startedAt)
|
.add("startsAt" -> rt.round.startsAt)
|
||||||
.add("finished" -> r.finished.option(true))
|
.add("startedAt" -> rt.round.startedAt)
|
||||||
|
.add("finished" -> rt.round.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,13 +20,10 @@ 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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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'
|
||||||
)
|
)
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
@import 'widget';
|
|
||||||
|
|
||||||
.relay-index {
|
|
||||||
.box__top {
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new::before {
|
|
||||||
font-size: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
margin-bottom: 2em;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue