From 592b776fef88cfa7907df1c8eac97b05cebe6225 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sat, 24 Apr 2021 10:41:32 +0200 Subject: [PATCH] broadcast tournament/round WIP --- app/controllers/RelayRound.scala | 47 ++++++++------ .../relay/{form.scala => roundForm.scala} | 12 ++-- app/views/relay/tour.scala | 2 + bin/mongodb/relay-tour-migration.js | 17 +++-- bin/mongodb/remove-relay.js | 11 ---- modules/relay/src/main/BSONHandlers.scala | 5 +- modules/relay/src/main/RelayApi.scala | 64 ++++++++++--------- modules/relay/src/main/RelayPager.scala | 8 ++- modules/relay/src/main/RelayRound.scala | 1 + modules/relay/src/main/RelayRoundForm.scala | 33 ++++++---- modules/relay/src/main/RelayRoundRepo.scala | 10 ++- modules/relay/src/main/RelayTour.scala | 6 +- modules/relay/src/main/RelayTourForm.scala | 2 +- modules/study/src/main/ChapterRepo.scala | 10 +-- 14 files changed, 127 insertions(+), 101 deletions(-) rename app/views/relay/{form.scala => roundForm.scala} (88%) delete mode 100644 bin/mongodb/remove-relay.js diff --git a/app/controllers/RelayRound.scala b/app/controllers/RelayRound.scala index 887f7bc73b..f3acdedc35 100644 --- a/app/controllers/RelayRound.scala +++ b/app/controllers/RelayRound.scala @@ -19,9 +19,9 @@ final class RelayRound( def form(tourId: String) = Auth { implicit ctx => me => NoLameOrBot { - WithTour(tourId) { tour => - (tour.owner == me.id) ?? { - Ok(html.relay.form.create(env.relay.roundForm.create, tour)).fuccess + WithTourAndRounds(tourId) { trs => + (trs.tour.ownerId == me.id) ?? { + Ok(html.relay.roundForm.create(env.relay.roundForm.create(trs), trs.tour)).fuccess } } } @@ -32,15 +32,16 @@ final class RelayRound( auth = implicit ctx => me => NoLameOrBot { - WithTour(tourId) { tour => - (tour.owner == me.id) ?? { - env.relay.roundForm.create + WithTourAndRounds(tourId) { trs => + (trs.tour.ownerId == me.id) ?? { + env.relay.roundForm + .create(trs) .bindFromRequest()(ctx.body, formBinding) .fold( - err => BadRequest(html.relay.form.create(err, tour)).fuccess, + err => BadRequest(html.relay.roundForm.create(err, trs.tour)).fuccess, setup => - env.relay.api.create(setup, me, tour) map { relay => - Redirect(relay.withTour(tour).path) + env.relay.api.create(setup, me, trs.tour) map { relay => + Redirect(routes.RelayTour.show(trs.tour.slug, trs.tour.id.value)) } ) } @@ -50,13 +51,16 @@ final class RelayRound( me => env.relay.api tourById TourModel.Id(tourId) flatMap { _ ?? { tour => - !(me.isBot || me.lame) ?? - env.relay.roundForm.create - .bindFromRequest()(req, formBinding) - .fold( - err => BadRequest(apiFormError(err)).fuccess, - setup => env.relay.api.create(setup, me, tour) map env.relay.jsonView.admin map JsonOk - ) + env.relay.api.withRounds(tour) flatMap { trs => + !(me.isBot || me.lame) ?? + env.relay.roundForm + .create(trs) + .bindFromRequest()(req, formBinding) + .fold( + err => BadRequest(apiFormError(err)).fuccess, + setup => env.relay.api.create(setup, me, tour) map env.relay.jsonView.admin map JsonOk + ) + } } } ) @@ -64,7 +68,7 @@ final class RelayRound( def edit(id: String) = Auth { implicit ctx => me => OptionFuResult(env.relay.api.byIdAndContributor(id, me)) { rt => - Ok(html.relay.form.edit(rt, env.relay.roundForm.edit(rt.relay))).fuccess + Ok(html.relay.roundForm.edit(rt, env.relay.roundForm.edit(rt.relay))).fuccess } } @@ -77,7 +81,7 @@ final class RelayRound( case Some(res) => res .fold( - { case (old, err) => BadRequest(html.relay.form.edit(old, err)) }, + { case (old, err) => BadRequest(html.relay.roundForm.edit(old, err)) }, rt => Redirect(rt.path) ) .fuccess @@ -189,6 +193,13 @@ final class RelayRound( )(implicit ctx: Context): Fu[Result] = OptionFuResult(env.relay.api tourById TourModel.Id(id))(f) + private def WithTourAndRounds(id: String)( + f: TourModel.WithRounds => Fu[Result] + )(implicit ctx: Context): Fu[Result] = + WithTour(id) { tour => + env.relay.api withRounds tour flatMap f + } + private def doShow(rt: RoundModel.WithTour, oldSc: lila.study.Study.WithChapter)(implicit ctx: Context ): Fu[Result] = diff --git a/app/views/relay/form.scala b/app/views/relay/roundForm.scala similarity index 88% rename from app/views/relay/form.scala rename to app/views/relay/roundForm.scala index 8038e165f3..52cbdb6535 100644 --- a/app/views/relay/form.scala +++ b/app/views/relay/roundForm.scala @@ -10,20 +10,20 @@ import lila.relay.RelayRound.Sync.UpstreamUrl.LccRegex import lila.relay.RelayRoundForm.Data import lila.relay.{ RelayRound, RelayTour } -object form { +object roundForm { import trans.broadcast._ def create(form: Form[Data], tour: RelayTour)(implicit ctx: Context) = layout(newBroadcast.txt())( - h1(newBroadcast()), - inner(form, routes.RelayRound.create(tour.id.value)) + h1(a(href := views.html.relay.tour.url(tour))(tour.name), " • ", addRound()), + inner(form, routes.RelayRound.create(tour.id.value), tour) ) def edit(rt: RelayRound.WithTour, form: Form[Data])(implicit ctx: Context) = layout(rt.fullName)( h1("Edit ", rt.fullName), - inner(form, routes.RelayRound.update(rt.relay.id.value)), + inner(form, routes.RelayRound.update(rt.relay.id.value), rt.tour), hr, postForm(action := routes.RelayRound.cloneRelay(rt.relay.id.value))( submitButton( @@ -49,7 +49,7 @@ object form { 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], url: play.api.mvc.Call, tour: RelayTour)(implicit ctx: Context) = postForm(cls := "form3", action := url)( div(cls := "form-group")( a(dataIcon := "", cls := "text", href := routes.Page.loneBookmark("broadcasts"))( @@ -101,7 +101,7 @@ object form { ), isGranted(_.Relay) option form3.group(form("credit"), credits())(form3.input(_)), form3.actions( - a(href := routes.RelayTour.index(1))(trans.cancel()), + a(href := views.html.relay.tour.url(tour))(trans.cancel()), form3.submit(trans.apply()) ) ) diff --git a/app/views/relay/tour.scala b/app/views/relay/tour.scala index b531eb8792..aad0e960c7 100644 --- a/app/views/relay/tour.scala +++ b/app/views/relay/tour.scala @@ -31,4 +31,6 @@ object tour { ) ) } + + def url(t: RelayTour) = routes.RelayTour.show(t.slug, t.id.value) } diff --git a/bin/mongodb/relay-tour-migration.js b/bin/mongodb/relay-tour-migration.js index 839577ef7e..7e622320f2 100644 --- a/bin/mongodb/relay-tour-migration.js +++ b/bin/mongodb/relay-tour-migration.js @@ -9,13 +9,16 @@ const newId = () => { db.relay.find().forEach(relay => { const tourId = newId(); db.relay_tour.insert({ - _id: tourId, - name: relay.name, - description: relay.description, - markup: relay.markup, - ownerId: relay.ownerId, - createdAt: relay.createdAt, - official: relay.official, + ...{ + _id: tourId, + order: NumberInt(0), + name: relay.name, + description: relay.description, + ownerId: relay.ownerId, + createdAt: relay.createdAt, + official: relay.official, + }, + ...(relay.markup ? { markup: relay.markup } : {}), }); db.relay.update( { _id: relay._id }, diff --git a/bin/mongodb/remove-relay.js b/bin/mongodb/remove-relay.js deleted file mode 100644 index dfaa8c98c3..0000000000 --- a/bin/mongodb/remove-relay.js +++ /dev/null @@ -1,11 +0,0 @@ -var id = 'RWtJ4aKC'; - -var relay = db.relay.findOne({ _id: id }); -if (relay) { - print('Removing ' + relay.games.length + ' games'); - relay.games.forEach(function (g) { - db.game5.remove({ _id: g.id }); - }); - print('Removing relay'); - db.relay.remove({ _id: id }); -} diff --git a/modules/relay/src/main/BSONHandlers.scala b/modules/relay/src/main/BSONHandlers.scala index 51113a4e5e..62c0ed46a1 100644 --- a/modules/relay/src/main/BSONHandlers.scala +++ b/modules/relay/src/main/BSONHandlers.scala @@ -38,7 +38,8 @@ object BSONHandlers { def readRelayWithTour(doc: Bdoc): Option[RelayRound.WithTour] = for { - relay <- doc.asOpt[RelayRound] - tour <- doc.getAsOpt[RelayTour]("tour") + _ <- Some(println(lila.db.BSON debug doc)) + relay <- doc.asOpt[RelayRound].pp + tour <- doc.getAsTry[RelayTour]("tour").pp.toOption } yield RelayRound.WithTour(relay, tour) } diff --git a/modules/relay/src/main/RelayApi.scala b/modules/relay/src/main/RelayApi.scala index a07b86f8ed..7c169e82fc 100644 --- a/modules/relay/src/main/RelayApi.scala +++ b/modules/relay/src/main/RelayApi.scala @@ -15,7 +15,7 @@ import lila.study.{ Settings, Study, StudyApi, StudyMaker, StudyRepo } import lila.user.User final class RelayApi( - relayRepo: RelayRoundRepo, + roundRepo: RelayRoundRepo, tourRepo: RelayTourRepo, studyApi: StudyApi, studyRepo: StudyRepo, @@ -26,10 +26,10 @@ final class RelayApi( import BSONHandlers._ import lila.study.BSONHandlers.StudyBSONHandler - def byId(id: RelayRound.Id) = relayRepo.coll.byId[RelayRound](id.value) + def byId(id: RelayRound.Id) = roundRepo.coll.byId[RelayRound](id.value) def byIdWithTour(id: RelayRound.Id): Fu[Option[RelayRound.WithTour]] = - relayRepo.coll + roundRepo.coll .aggregateOne() { framework => import framework._ Match($id(id)) -> List( @@ -42,7 +42,8 @@ final class RelayApi( def byIdAndContributor(id: RelayRound.Id, me: User) = byIdWithStudy(id) map { _ collect { - case RelayRound.WithTourAndStudy(relay, tour, study) if study.canContribute(me.id) => relay withTour tour + case RelayRound.WithTourAndStudy(relay, tour, study) if study.canContribute(me.id) => + relay withTour tour } } @@ -56,7 +57,9 @@ final class RelayApi( } def byTour(tour: RelayTour): Fu[List[RelayRound.WithTour]] = - relayRepo.byTour(tour).dmap(_.map(_ withTour tour)) + roundRepo.byTour(tour).dmap(_.map(_ withTour tour)) + + def withRounds(tour: RelayTour) = roundRepo.byTour(tour).dmap(tour.withRounds) def fresh(me: Option[User]): Fu[RelayRound.Fresh] = fuccess(RelayRound.Fresh(Nil, Nil)) // relayRepo.scheduled.flatMap(withStudy andLiked me) zip @@ -76,7 +79,7 @@ final class RelayApi( ) def fetchWithTours(query: Bdoc, maxDocs: Int, readPreference: ReadPreference = ReadPreference.primary) = - relayRepo.coll + roundRepo.coll .aggregateList(maxDocs, readPreference) { framework => import framework._ Match(query) -> List( @@ -91,25 +94,26 @@ final class RelayApi( tourRepo.coll.insert.one(tour) inject tour } - def create(data: RelayRoundForm.Data, user: User, tour: RelayTour): Fu[RelayRound] = { - val relay = data.make(user, tour) - relayRepo.coll.insert.one(relay) >> - studyApi.importGame( - StudyMaker.ImportGame( - id = relay.studyId.some, - name = Study.Name(relay.name).some, - settings = Settings.init - .copy( - chat = Settings.UserSelection.Everyone, - sticky = false - ) - .some, - from = Study.From.Relay(none).some - ), - user - ) >> - studyApi.addTopics(relay.studyId, List("Broadcast")) inject relay - } + def create(data: RelayRoundForm.Data, user: User, tour: RelayTour): Fu[RelayRound] = + roundRepo.nextOrderByTour(tour.id) flatMap { order => + val relay = data.make(user, tour, order) + roundRepo.coll.insert.one(relay) >> + studyApi.importGame( + StudyMaker.ImportGame( + id = relay.studyId.some, + name = Study.Name(relay.name).some, + settings = Settings.init + .copy( + chat = Settings.UserSelection.Everyone, + sticky = false + ) + .some, + from = Study.From.Relay(none).some + ), + user + ) >> + studyApi.addTopics(relay.studyId, List("Broadcast")) inject relay + } def requestPlay(id: RelayRound.Id, v: Boolean): Funit = WithRelay(id) { relay => @@ -126,7 +130,7 @@ final class RelayApi( studyApi.rename(relay.studyId, Study.Name(relay.name)) >> { if (relay == from) fuccess(relay) else - relayRepo.coll.update.one($id(relay.id), relay).void >> { + roundRepo.coll.update.one($id(relay.id), relay).void >> { (relay.sync.playing != from.sync.playing) ?? publishRelay(relay) } >>- { relay.sync.log.events.lastOption.ifTrue(relay.sync.log != from.sync.log).foreach { event => @@ -148,7 +152,7 @@ final class RelayApi( ) def getOngoing(id: RelayRound.Id): Fu[Option[RelayRound.WithTour]] = - relayRepo.coll.one[RelayRound]($doc("_id" -> id, "finished" -> false)) flatMap { + roundRepo.coll.one[RelayRound]($doc("_id" -> id, "finished" -> false)) flatMap { _ ?? { relay => tourById(relay.tourId) map2 relay.withTour } @@ -162,7 +166,7 @@ final class RelayApi( // .map(jsonView.public) private[relay] def autoStart: Funit = - relayRepo.coll.list[RelayRound]( + roundRepo.coll.list[RelayRound]( $doc( "startsAt" $lt DateTime.now.plusMinutes(30) // start 30 minutes early to fetch boards $gt DateTime.now.minusDays(1), // bit late now @@ -177,7 +181,7 @@ final class RelayApi( } private[relay] def autoFinishNotSyncing: Funit = - relayRepo.coll.list[RelayRound]( + roundRepo.coll.list[RelayRound]( $doc( "sync.until" $exists false, "finished" -> false, @@ -198,7 +202,7 @@ final class RelayApi( byId(id) flatMap { _ ?? f } private[relay] def onStudyRemove(studyId: String) = - relayRepo.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) diff --git a/modules/relay/src/main/RelayPager.scala b/modules/relay/src/main/RelayPager.scala index 121cdd59c1..e6321c39ca 100644 --- a/modules/relay/src/main/RelayPager.scala +++ b/modules/relay/src/main/RelayPager.scala @@ -39,12 +39,14 @@ final class RelayPager(roundRepo: RelayRoundRepo, tourRepo: RelayTourRepo, study roundRepo.coll .aggregateList(length, readPreference = ReadPreference.secondaryPreferred) { framework => import framework._ - Match(selector) -> List( - Sort(Descending("startedAt")), + Sort(Descending("startedAt")) -> List( + GroupField("tourId")("round" -> FirstField("$ROOT")), Skip(offset), Limit(length), + ReplaceRootField("round"), + AddFields($doc("sync.log" -> $arr())), PipelineOperator(tourRepo lookup "tourId"), - Unwind("tour") + UnwindField("tour") ) } .map(_ flatMap readRelayWithTour) diff --git a/modules/relay/src/main/RelayRound.scala b/modules/relay/src/main/RelayRound.scala index e9c7801b16..4e30f75388 100644 --- a/modules/relay/src/main/RelayRound.scala +++ b/modules/relay/src/main/RelayRound.scala @@ -8,6 +8,7 @@ import lila.user.User case class RelayRound( _id: RelayRound.Id, tourId: RelayTour.Id, + order: Int, name: String, description: String, markup: Option[String] = None, diff --git a/modules/relay/src/main/RelayRoundForm.scala b/modules/relay/src/main/RelayRoundForm.scala index 1e1b8b408e..2b3ed61310 100644 --- a/modules/relay/src/main/RelayRoundForm.scala +++ b/modules/relay/src/main/RelayRoundForm.scala @@ -17,7 +17,7 @@ final class RelayRoundForm { import RelayRoundForm._ import lila.common.Form.ISODateTimeOrTimestamp - val form = Form( + val roundMapping = mapping( "name" -> cleanText(minLength = 3, maxLength = 80), "description" -> cleanText(minLength = 3, maxLength = 400), @@ -31,11 +31,21 @@ final class RelayRoundForm { "throttle" -> optional(number(min = 2, max = 60)) )(Data.apply)(Data.unapply) .verifying("This source requires a round number. See the new form field below.", !_.roundMissing) + + def create(trs: RelayTour.WithRounds) = Form { + roundMapping + .verifying( + s"Maximum rounds per tournament: ${RelayTour.maxRelays}", + _ => trs.rounds.sizeIs < RelayTour.maxRelays + ) + }.fill( + Data( + name = s"Round ${trs.rounds.size + 1}", + description = "" + ) ) - def create = form - - def edit(r: RelayRound) = form fill Data.make(r) + def edit(r: RelayRound) = Form(roundMapping) fill Data.make(r) } object RelayRoundForm { @@ -84,12 +94,12 @@ object RelayRoundForm { case class Data( name: String, description: String, - markup: Option[String], - syncUrl: Option[String], - syncUrlRound: Option[Int], - credit: Option[String], - startsAt: Option[DateTime], - throttle: Option[Int] + markup: Option[String] = None, + syncUrl: Option[String] = None, + syncUrlRound: Option[Int] = None, + credit: Option[String] = None, + startsAt: Option[DateTime] = None, + throttle: Option[Int] = None ) { def requiresRound = syncUrl exists RelayRound.Sync.UpstreamUrl.LccRegex.matches @@ -129,10 +139,11 @@ object RelayRoundForm { log = SyncLog.empty ) - def make(user: User, tour: RelayTour) = + def make(user: User, tour: RelayTour, order: Int) = RelayRound( _id = RelayRound.makeId, tourId = tour.id, + order = order, name = name, description = description, markup = markup, diff --git a/modules/relay/src/main/RelayRoundRepo.scala b/modules/relay/src/main/RelayRoundRepo.scala index 5551668582..091145af7f 100644 --- a/modules/relay/src/main/RelayRoundRepo.scala +++ b/modules/relay/src/main/RelayRoundRepo.scala @@ -32,14 +32,20 @@ final private class RelayRoundRepo(val coll: Coll)(implicit ec: scala.concurrent // .batchSize(batchSize) // .cursor[Relay](ReadPreference.secondaryPreferred) + def nextOrderByTour(tourId: RelayTour.Id): Fu[Int] = + coll.primitiveOne[Int](selectors.tour(tourId), orderSort, "order") dmap { ~_ + 1 } + def byTour(tour: RelayTour): Fu[List[RelayRound]] = coll - .find($doc("tourId" -> tour.id)) - .sort($sort desc "startsAt") + .find(selectors.tour(tour.id)) + .sort(orderSort) .cursor[RelayRound]() .list(RelayTour.maxRelays) + private val orderSort = $sort asc "order" + private[relay] object selectors { + def tour(id: RelayTour.Id) = $doc("tourId" -> id) // def scheduled(official: Boolean) = // officialOption(official) ++ $doc( // "startsAt" $gt DateTime.now.minusHours(1), diff --git a/modules/relay/src/main/RelayTour.scala b/modules/relay/src/main/RelayTour.scala index 8972a0388c..5411c24db0 100644 --- a/modules/relay/src/main/RelayTour.scala +++ b/modules/relay/src/main/RelayTour.scala @@ -9,7 +9,7 @@ case class RelayTour( name: String, description: String, markup: Option[String] = None, - owner: User.ID, + ownerId: User.ID, official: Boolean, createdAt: DateTime ) { @@ -19,6 +19,8 @@ case class RelayTour( val s = lila.common.String slugify name if (s.isEmpty) "-" else s } + + def withRounds(rounds: List[RelayRound]) = RelayTour.WithRounds(this, rounds) } object RelayTour { @@ -27,5 +29,7 @@ object RelayTour { case class Id(value: String) extends AnyVal with StringValue + case class WithRounds(tour: RelayTour, rounds: List[RelayRound]) + def makeId = Id(lila.common.ThreadLocalRandom nextString 8) } diff --git a/modules/relay/src/main/RelayTourForm.scala b/modules/relay/src/main/RelayTourForm.scala index 4e7e592868..0009b85dd9 100644 --- a/modules/relay/src/main/RelayTourForm.scala +++ b/modules/relay/src/main/RelayTourForm.scala @@ -53,7 +53,7 @@ object RelayTourForm { name = name, description = description, markup = markup, - owner = user.id, + ownerId = user.id, official = ~official && Granter(_.Relay)(user), createdAt = DateTime.now ) diff --git a/modules/study/src/main/ChapterRepo.scala b/modules/study/src/main/ChapterRepo.scala index 5774cd98c0..4ebcb00a13 100644 --- a/modules/study/src/main/ChapterRepo.scala +++ b/modules/study/src/main/ChapterRepo.scala @@ -96,15 +96,7 @@ final class ChapterRepo(val coll: AsyncColl)(implicit } def nextOrderByStudy(studyId: Study.Id): Fu[Int] = - coll( - _.primitiveOne[Int]( - $studyId(studyId), - $sort desc "order", - "order" - ) - ) dmap { order => - ~order + 1 - } + coll(_.primitiveOne[Int]($studyId(studyId), $sort desc "order", "order")) dmap { ~_ + 1 } def setConceal(chapterId: Chapter.Id, conceal: Chapter.Ply) = coll(_.updateField($id(chapterId), "conceal", conceal)).void